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...

I tried to reduce my boilerplate when writing repositories and ended up with a free additonal safety check
Photo by Joe Dudeck / Unsplash

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!