This library is currently in alpha. While the core API is mostly stable and unlikely to change significantly before the final release, the project is still under development and has limited real-world usage. Bugs, crashes, or unexpected behavior may occur.
We're excited to announce the first alpha release of Python Object Mapper (POM), a flexible and powerful object mapping library for Python.
- ✨ Map attributes between objects with identical property names
- 🔄 Support for Python
dataclasses - 🔍 Automatic handling of public attributes and initialization parameters
- ⚡️ Smart type detection and validation
- 🛠 Transform property values using custom functions
- 🔄 Map properties with different names between objects
- 🔗 Map from multiple source objects to a single target
- 🚫 Support for property exclusions
- ➕ Add extra properties during mapping
- ⏩ Skip initialization when needed
- ⛓ Chain-based attribute resolution
- 🎯 Support for both class and instance-based mapping
- 💾 Default value preservation
- 🏷 Property decorator support
- 📝 Descriptive error messages for missing attributes
- ✅ Validation of mapping configurations
- 🛡 Guards against excluded required attributes
- ❌ Clear error messages for mapping failures
POM provides optional support for mapping objects that use Pydantic's BaseModel. If Pydantic is installed in your environment, POM will automatically detect and handle Pydantic models during mapping. This includes:
- Extracting attributes from Pydantic models.
- Mapping between Pydantic models and other object types.
Note: Pydantic is not a required dependency for POM. You can use POM without installing Pydantic. If Pydantic is not installed, POM will gracefully fall back to handling plain Python objects and dataclasses.
pip install git+https://github.com/Arbeit-Studio/pom.git- Python 3.9 or higher
- Alpha release — API may change before final release
- Limited real-world testing
- Performance optimization pending for large object mappings
We welcome contributions! Please check our GitHub repository for guidelines.
Apache License 2.0
For usage examples and detailed documentation, please refer to the README.md.
Note: This is an alpha release. Please report any issues on our GitHub repository.
Sample classes.
class A:
def __init__(self, name: str, email: str):
self.name = name
self.email = email
class B:
def __init__(self, name: str, email:str, age: int):
self.name = name
self.email = email
self.age = age
class C:
def __init__(self, name: str, email: str, age: int):
self.name = name
self.email = email
self.age = age- Define the mapping.
from pom import Mapper
mapper = Mapper()
mapper.add_mapping(
source=A,
target=B,
mapping={'name': lambda n: n[::-1]}
)- Map the object
a = A('Johnny', '[email protected]')
b = mapper.map(a, B, extra={"age": 35})- The result
>>> print(vars(b))
{'name': 'ynnhoJ', 'email': '[email protected]', 'age': 35}- Define the mapping as a tuple of two classes as source
(A, B).
mapper = Mapper()
mapper.add_mapping(
source=(A,B),
target=C,
mapping={'name': lambda n: n[::-1]}
)- Map the objects as usual.
a = A('Johnny', '[email protected]')
b = B('Jodin', '[email protected]', 30)
c = mapper.map((a,b), C)Objects in the left of the source tuple have precedence in the attribute discovery from the map method.
- The result.
>>> print(vars(c))
{'name': 'ynnhoJ', 'email': '[email protected]', 'age': 30}The Mapper class provides flexible object-to-object mapping with support for transformations, exclusions, and multiple source objects.
Map properties between objects with identical attribute names:
from pom import Mapper
class Source:
def __init__(self, name: str, email: str):
self.name = name
self.email = email
class Target:
def __init__(self, name: str, email: str):
self.name = name
self.email = email
mapper = Mapper()
mapper.add_mapping(source=Source, target=Target)
source = Source("John", "[email protected]")
result: Target = mapper.map(source, Target)Apply transformations to properties during mapping:
def reverse_string(s: str) -> str:
return s[::-1]
mapper.add_mapping(
source=Source,
target=Target,
mapping={"name": reverse_string}
)Map properties with different names:
class Target:
def __init__(self, full_name: str, email_address: str):
self.full_name = full_name
self.email_address = email_address
mapper.add_mapping(
source=Source,
target=Target,
mapping={
"name": "full_name",
"email": "email_address"
}
)Apply both transformations and name translations:
mapper.add_mapping(
source=Source,
target=Target,
mapping={
"name": ("full_name", reverse_string),
"email": "email_address"
}
)Map from multiple source objects to a single target:
class SourceA:
def __init__(self, name: str):
self.name = name
class SourceB:
def __init__(self, email: str, age: int):
self.email = email
self.age = age
class Target:
def __init__(self, name: str, email: str, age: int):
self.name = name
self.email = email
self.age = age
mapper.add_mapping(
source=(SourceA, SourceB),
target=Target
)
a = SourceA("John")
b = SourceB("[email protected]", 30)
result = mapper.map((a, b), Target)Note: Objects earlier in the source tuple take precedence for overlapping attributes.
Exclude specific properties from mapping:
mapper.add_mapping(
source=Source,
target=Target,
exclusions=["email"]
)Provide additional properties during mapping:
result = mapper.map(
source,
Target,
extra={"age": 30}
)The params on the extra map overrides attributes copied from the source.
Map between dataclass objects:
from dataclasses import dataclass
@dataclass
class SourceData:
name: str
email: str
@dataclass
class TargetData:
name: str
email: str
mapper.add_mapping(source=SourceData, target=TargetData)Map to existing target instances:
source = Source("John", "[email protected]")
target = Target(None, None)
mapper.add_mapping(source=Source, target=Target)
mapper.map(source, target) # Updates existing target instanceMap between Pydantic models and other object types:
from pydantic import BaseModel
class UserModel(BaseModel):
name: str
email: str
age: int
class UserDTO:
def __init__(self, name: str, email: str, age: int):
self.name = name
self.email = email
self.age = age
mapper = Mapper()
mapper.add_mapping(source=UserModel, target=UserDTO)
user_model = UserModel(name="John", email="[email protected]", age=30)
user_dto = mapper.map(user_model, UserDTO)POM automatically handles Pydantic model attributes during mapping. You can also apply transformations:
mapper.add_mapping(
source=UserModel,
target=UserDTO,
mapping={"name": lambda n: n.upper()}
)When working with Pydantic's models as targets, pay attention to its aliases. POM does not discover the correct mapping automatically, which may result in attributes being skipped or incorrectly mapped. For example, if a Pydantic model uses aliases like firstName for first_name, POM will not map these attributes unless explicitly specified. To correctly map the object in this scenario, you should provide the aliases as names in the mapping dict to the add_mapping method.
Providing the right aliases as mapping:
from pydantic import BaseModel, Field
class UserDTO:
def __init__(self, first_name: str, last_name: str, email_address: str):
self.first_name = first_name
self.last_name = last_name
self.email_address = email_address
class UserModel(BaseModel):
first_name: str = Field(alias="firstName")
last_name: str = Field(alias="lastName")
email_address: str = Field(alias="emailAddress")
mapper = Mapper()
mapper.add_mapping(
source=UserDTO,
target=UserModel,
mapping={
"first_name": "firstName",
"last_name": "lastName",
"email_address": "emailAddress"
}
)
user_model = UserModel(
firstName="John",
lastName="Doe",
emailAddress="[email protected]"
)
user_dto = mapper.map(user_model, UserDTO)Initialize a new Mapper instance.
Add a mapping configuration.
Parameters:
source: Source class or tuple of source classestarget: Target classmapping: Dict of property mappings or list of property namesexclusions: Set of properties names to exclude from mapping
Map source object(s) to target.
Parameters:
source: Source object or tuple of objectstarget: Target class or instanceskip_init: Skip init when creating target instanceextra: Additional attributes to set on target
Returns:
- Instance of target type with mapped properties
The Mapper provides descriptive error messages for common issues:
- Missing required properties
- Invalid mapping configurations
- Excluded required properties
- Invalid transformation functions
Example error handling:
try:
mapper.add_mapping(source=Source, target=Target)
result: Target = mapper.map(source, target)
except TypeError as e:
print(f"Mapping error: {e}")
except RuntimeError as e:
print(f"Configuration error: {e}")