4.3 KiB
| title | aliases | tags | sources | created | updated | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| FastAPI ORM @property for JSON-Column Fields — No Migration Needed |
|
|
|
2026-05-07 | 2026-05-07 |
FastAPI ORM @property for JSON-Column Fields — No Migration Needed
When an ORM model already stores data in a JSON column (e.g. fields_json), new read-only display fields can be exposed via Python @property on the model class — zero Alembic migration, zero schema change, forward-compatible.
Key Points
- If the data already exists inside a JSON column, a
@propertysurfaces it as a first-class attribute on the model - No
ALTER TABLE, no Alembic revision, no downtime — the column is already there - Works seamlessly with FastAPI's
response_model: add the field to the Pydantic schema and it resolves via the property - Use this pattern for read-only derived or display fields; for fields that need filtering/ordering in SQL, a real column is still required
- The property must be declared with a
@propertydecorator; SQLAlchemy does not auto-expose JSON sub-keys
Pattern
from sqlalchemy import Column, String, JSON
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
class Task(Base):
__tablename__ = "tasks"
id = Column(String, primary_key=True)
title = Column(String)
fields_json = Column(JSON, default=dict) # existing JSON column
# ← New display fields via @property — no migration needed
@property
def tags(self) -> list[str]:
return (self.fields_json or {}).get("tags", [])
@property
def priority(self) -> str | None:
return (self.fields_json or {}).get("priority")
Pydantic Schema
from pydantic import BaseModel, ConfigDict
class TaskResponse(BaseModel):
model_config = ConfigDict(from_attributes=True) # enables ORM mode
id: str
title: str
tags: list[str] = [] # ← resolved from ORM @property
priority: str | None = None
[!info]
from_attributes=Trueis required Pydantic v2from_attributes=True(previouslyorm_mode = Truein v1) allows the schema to read Python object attributes, including@property, not just dict keys.
Pydantic v2 Gotcha — @property Is NOT Serialized
In Pydantic v2, a plain @property on a Pydantic model class is not included in .model_dump() or JSON serialization. The property must be declared as @computed_field to appear in the API response.
# WRONG in Pydantic v2 — property not serialized
class TaskResponse(BaseModel):
fields_json: dict = {}
@property
def tags(self) -> list[str]:
return self.fields_json.get("tags", [])
# tags will NOT appear in .model_dump() or JSON response
# CORRECT — use @computed_field
from pydantic import computed_field
class TaskResponse(BaseModel):
fields_json: dict = {}
@computed_field
@property
def tags(self) -> list[str]:
return self.fields_json.get("tags", [])
# tags WILL appear in .model_dump() and JSON response
Note: @computed_field is a Pydantic v2 feature — not available in Pydantic v1.
For SQLAlchemy ORM models (not Pydantic models), @property works fine because Pydantic reads attributes via from_attributes=True. The @computed_field workaround is only needed when the property is on the Pydantic schema class itself.
When to Use
| Scenario | Use @property |
Use real column |
|---|---|---|
| Display-only field, data already in JSON | ✓ | — |
Field needs SQL WHERE/ORDER BY |
— | ✓ |
| No Alembic migration budget | ✓ | — |
| Aggregation or JOIN on this field | — | ✓ |
Related Concepts
- wiki/concepts/fastapi-response-model-silent-field-strip — FastAPI schema omission silently strips fields; adding a
@propertyonly helps if the field is also in the Pydantic model - wiki/tech-patterns/fastapi-python-docker — FastAPI deployment context
Sources
- daily/2026-05-07.md — Session 12:09: Vue 3 + FastAPI + Planka + Azure DevOps integration;
tagsandado_work_item_idsurfaced via@propertyfromfields_jsonwithout adding DB columns