How to optionally prevent/exclude an sqlalchemy relationship field from being loaded during Pydantic model validation

I am using FastAPI with SQLAlchemy ORM and Pydantic models. I’ve encountered an issue where lazy-loaded ORM fields are inadvertently loaded by Pydantic’s model_validate method. I want to exclude certain fields when loading a model using model_validate, particularly for cases where I need to prevent the loading of lazily loaded relationships.

With the release of Pydantic v2, is it possible to load a model and exclude certain fields when using the new model_validate method? I thought the context parameter might help, but it doesn’t seem to do what I need.

Here’s a simplified example of my code structure:

# SQLAlchemy ORM model
class RoleOrm(OrmBase):
    __tablename__ = "roles"
    # Columns...
    role_permissions = relationship(..., lazy="joined")

# Pydantic model
class Role(BaseModel):
    # Fields...
    role_permissions: List[RolePermission] | None

# Repository
class RoleRepository:
    @staticmethod
    async def get_by_uid(db: AsyncSession, uid: str) -> Role:
        statement = select(RoleOrm).where(RoleOrm.uid == uid)
        results = await db.execute(statement)
        role_orm = results.unique().scalar_one_or_none()
        if not role_orm:
            raise HTTPException(
                status_code=404,
                detail="Role with given id does not exist",
            )
        role = Role.model_validate(role_orm)
        return role

    @staticmethod
    async def get_many(
        db: AsyncSession, skip: int = 0, limit: int = 20
    ) -> tuple[list[Role], str]:
        count = await db.scalar(select(func.count(RoleOrm.uid)))
        if not count:
            return [], "0"
        statement = (
            select(RoleOrm)
            .offset(skip)
            .limit(limit)
        )
        results = await db.execute(statement)
        roles = [Role.model_validate(orm) for orm in results.unique().scalars().all()]
        return roles, str(count)

In get_many, I want to avoid loading role_permissions, but in get_by_uid, I need them included. How can I achieve this selective field exclusion in Pydantic’s model_validate?

Related GitHub issues:

  1. model_validate: exclude #8192
  2. ORM models and lazy loading. How to optionally prevent a field from being loaded?

I had a similar issue and solved it by using SQLAlchemy’s noload to explicitly prevent the loading of relationships in certain queries. This approach allows you to control relationship loading in your repository methods.

Here’s how I modified the repository to handle this:

from sqlalchemy.orm import noload

class RoleRepository:
    # Method to get a single role with permissions
    @staticmethod
    async def get_by_uid(db: AsyncSession, uid: str) -> Role:
        statement = select(RoleOrm).where(RoleOrm.uid == uid)
        role_orm = await db.execute(statement).scalar_one_or_none()
        if not role_orm:
            raise HTTPException(status_code=404, detail="Role not found")
        return Role.model_validate(role_orm)

    # Method to get multiple roles without permissions
    @staticmethod
    async def get_many(db: AsyncSession, skip: int, limit: int) -> tuple[list[Role], str]:
        count = await db.scalar(select(func.count(RoleOrm.uid)))
        statement = (
            select(RoleOrm)
            .options(noload(RoleOrm.role_permissions))
            .offset(skip)
            .limit(limit)
        )
        roles = [Role.model_validate(orm) for orm in await db.execute(statement).scalars().all()]
        return roles, str(count)

Leave a Comment