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_field
s. 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"}')```
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}'