pythonfastapipydantic

With Pydantic V2 and model_validate, how can I create a "computed field" from an attribute of an ORM model that IS NOT part of the Pydantic model


This context here is that I am using FastAPI and have a response_model defined for each of the paths. The endpoint code returns a SQLAlchemy ORM instance which is then passed, I believe, to model_validate. The response_model is a Pydantic model that filters out many of the ORM model attributes (internal ids and etc...) and performs some transformations and adds some computed_fields. This all works just fine so long as all the attributes you need are part of the Pydantic model. Seems like __pydantic_context__ along with model_config = ConfigDict(from_attributes=True, extra='allow') would be a great way to hold on to some of the extra attributes from the ORM model and use them to compute new fields, however, it seems that when model_validate is used to create the instance that __pydantic_context__ remains empty. Is there some trick to getting this behavior in a clean way?

I have a way to make this work, but it involves dynamically adding new attributes to my ORM model, which leaves me with a bad feeling and a big FIXME in my code.

Here is some code to illustrate the problem. Note that the second test case fails.

from typing import Any
from pydantic import BaseModel, ConfigDict, computed_field, model_validator


class Foo:

    def __init__(self):
        self.original_thing = "foo"


class WishThisWorked(BaseModel):
    """
    __pydantic_extra__ does not pick up the additional attributes when model_validate is used to instantiate
    """
    model_config = ConfigDict(from_attributes=True, extra='allow')

    @computed_field
    @property
    def computed_thing(self) -> str:
        try:
            return self.__pydantic_extra__["original_thing"] + "_computed"
        except Exception as e:
            print(e)

        return None


model = WishThisWorked(original_thing="bar")
print(f'WishThisWorked (original_thing="bar") worked: {model.computed_thing == "bar_computed"}')

# this is the case that I actually want to work
model_orm = WishThisWorked.model_validate(Foo())
print(f'WishThisWorked model_validate(Foo()) worked: {model.computed_thing == "foo_computed"}')


class WorksButKludgy(BaseModel):
    """
    I don't like having to modify the instance passed to model_validate
    """
    model_config = ConfigDict(from_attributes=True)

    computed_thing: str

    @model_validator(mode="before")
    @classmethod
    def _set_fields(cls, values: Any) -> Any:
        if type(values) is Foo:
            # This is REALLY gross
            values.computed_thing = values.original_thing + "_computed"
        elif type(values) is dict:
            values["computed_thing"] = values["original_thing"] + "_computed"
        return values


print(f'WorksButKludgy (original_thing="bar") worked: {model.computed_thing == "bar_computed"}')
model = WorksButKludgy(original_thing="bar")

model_orm = WorksButKludgy.model_validate(Foo())
print(f'WorksButKludgy model_validate(Foo()) worked: {model_orm.computed_thing == "foo_computed"}')```

Solution

  • What you could consider is having all the ORM attributes in your schema, but labelling them as excluded. Then you have access to all your ORM attributes when you want to use them in a computed field:

    from pydantic import BaseModel, Field, property, computed_field, ConfigDict
    from sqlalchemy.orm import declaritive_base
    from sqlalchemy import Column, Integer, String
    
    SqlBase = declaritive_base()
    
    class SqlModel(SqlBase):
        ID = Column(Integer)
        Name = Column(String)
    
    
    class SqlSchema(BaseModel):
        model_config = ConfigDict(from_attributes=True)
        ID: int = Field(exclude=True)
        Name: str = Field(...)
    
        @computed_field
        @property
        def id_name(self) -> str:
            return f'{self.ID}_{self.Name}'