---
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
