obsidian/wiki/concepts/fastapi-orm-property-json-column.md
2026-05-10 21:14:23 +01:00

3.1 KiB

title aliases tags sources created updated
FastAPI ORM @property for JSON-Column Fields — No Migration Needed
orm-property-json-column
sqlalchemy-json-property
fastapi-no-migration-field
fastapi
sqlalchemy
python
orm
json
migration
pattern
daily/2026-05-07.md
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 @property surfaces 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 @property decorator; 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=True is required Pydantic v2 from_attributes=True (previously orm_mode = True in v1) allows the schema to read Python object attributes, including @property, not just dict keys.

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

Sources

  • daily/2026-05-07.md — Session 12:09: Vue 3 + FastAPI + Planka + Azure DevOps integration; tags and ado_work_item_id surfaced via @property from fields_json without adding DB columns