Cheatsheet for converting ORM (or any attribute-bearing) instances to Pydantic.
Enable from_attributes
class UserOut(BaseModel):
id: int
email: str
name: str | None
model_config = {"from_attributes": True}
Now Pydantic reads via getattr(obj, "field_name").
Validate from SQLAlchemy
sa_user = await session.get(User, 1)
out = UserOut.model_validate(sa_user)
Works with SQLAlchemy declarative models, attrs, dataclasses, anything with attributes.
FastAPI integration
@app.get("/users/{id}", response_model=UserOut)
async def get_user(id: int, db: AsyncSession = Depends(get_db)):
return await db.get(User, id) # SQLAlchemy → UserOut auto
response_model=UserOut + from_attributes=True = automatic conversion.
Eager-load relationships first
stmt = select(User).options(selectinload(User.posts)).where(User.id == id)
user = await db.scalar(stmt)
# Then convert
out = UserOutWithPosts.model_validate(user)
Without eager-load: lazy-load in async fails (MissingGreenlet).
Nested
class PostOut(BaseModel):
id: int
title: str
model_config = {"from_attributes": True}
class UserOutWithPosts(BaseModel):
id: int
email: str
posts: list[PostOut]
model_config = {"from_attributes": True}
Both must have from_attributes=True.
Computed-on-output (using ORM hybrid_property)
# SQLAlchemy
class User(Base):
first: Mapped[str]
last: Mapped[str]
@hybrid_property
def full_name(self) -> str:
return f"{self.first} {self.last}"
# Pydantic
class UserOut(BaseModel):
full_name: str
model_config = {"from_attributes": True}
# When SA user → UserOut: full_name property read via getattr
Exclude sensitive fields
# Bad: returning SQLAlchemy model directly
@app.get("/users/{id}")
async def get_user(id: int, db = Depends(get_db)):
return await db.get(User, id) # may include password_hash
# Good: explicit response_model
@app.get("/users/{id}", response_model=UserOut)
async def get_user(id: int, db = Depends(get_db)):
return await db.get(User, id) # UserOut only has safe fields
Computed from related fields
class OrderOut(BaseModel):
id: int
items_count: int # not on SA model; provide computed
model_config = {"from_attributes": True}
@computed_field
@property
def items_count(self) -> int:
return len(self.items) # but where does self.items come from?
For computed_field on ORM-derived Pydantic: needs the source attr accessible. Eager-load.
Alias from ORM
class UserOut(BaseModel):
user_id: int = Field(alias="id") # SA model has `id`, Pydantic field is `user_id`
model_config = {"from_attributes": True, "populate_by_name": True}
Lazy ORM load + async pitfall
# BAD in async (lazy-load triggers sync IO)
user = await db.get(User, 1)
return UserOutWithPosts.model_validate(user)
# user.posts not eager-loaded → error
Always eager-load relationships before converting.
Convert lists
@app.get("/users", response_model=list[UserOut])
async def list_users(db = Depends(get_db)):
return (await db.execute(select(User))).scalars().all()
FastAPI converts each item.
Pure attrs / dataclass
from attrs import define
@define
class UserAttrs:
id: int
email: str
class UserOut(BaseModel):
id: int
email: str
model_config = {"from_attributes": True}
UserOut.model_validate(UserAttrs(id=1, email="[email protected]"))
Same mechanism.
When NOT to use from_attributes
- Plain dicts — use
model_validate({...})directly (default). - Strict input validation — re-define source schema.
Common mistakes
- Lazy ORM access in async → MissingGreenlet.
- ORM model returned directly without
response_model→ field leak. - Forgetting
model_config = {"from_attributes": True}. - Trying to validate from sync ORM in async-only handler → blocks loop.
Read this next
If you want my SA → Pydantic schema reference, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .