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

82 lines
3.1 KiB
Markdown

---
title: "FastAPI ORM @property for JSON-Column Fields — No Migration Needed"
aliases: [orm-property-json-column, sqlalchemy-json-property, fastapi-no-migration-field]
tags: [fastapi, sqlalchemy, python, orm, json, migration, pattern]
sources:
- "daily/2026-05-07.md"
created: 2026-05-07
updated: 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
```python
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
```python
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 | — | ✓ |
## Related Concepts
- [[wiki/concepts/fastapi-response-model-silent-field-strip]] — FastAPI schema omission silently strips fields; adding a `@property` only 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; `tags` and `ado_work_item_id` surfaced via `@property` from `fields_json` without adding DB columns