I tried to reduce my boilerplate when writing repositories and ended up with a free additonal safety check
An attempt to reduce repetitive object construction, resultet in more type safety for Python...
Mapping an ORM row to a domain object field by field is boilerplate that grows silently whenever the schema changes. My previous implementation in SQLiteUserRepository named every field explicitly:
return User.from_(user_id=row.user_id, name=row.name, email=row.email)
The replacement unpacks the ORM row's __dict__ directly into User.from_():
row.__dict__.pop("_sa_instance_state", None)
return User.from_(**row.__dict__)
Less typing is the obvious gain, but the more valuable effect comes from model_config = ConfigDict(extra="forbid") on the User entity.
from pydantic import BaseModel, field_validator, ConfigDict
from validate_with_resolute import Record
class User(BaseModel, Record):
user_id: str
name: str
email: str
model_config = ConfigDict(
frozen=True,
extra= "forbid",
)
@field_validator("email")
def validate_email(cls, value: str) -> str:
if not "@" in value:
raise ValueError("Email must contain @")
return value
Pydantic will now raise a ValidationError if row.__dict__ contains a field that User does not declare, and fail to construct at all if User expects a field that the ORM row no longer provides. A column added to UserORM without a matching addition to the domain model, or vice versa, is caught at the point of construction rather than silently producing a partial or incorrect object somewhere downstream.
The one piece of overhead this introduces is the _sa_instance_state key that SQLAlchemy injects into every ORM instance's __dict__. It has to be removed before unpacking.
So the effort to reduce my boilerplate code when writing repositories, resulted in a free divergence check of my models - nice!