Skip to content

Schema Generation

Request and response schemas are generated automatically from the ORM model. There is no need to write ArticleCreate, ArticleUpdate, or ArticlePublic by hand.

What gets generated

Given this model:

class Article(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    title: str
    content: str
    published: bool = False
    created_at: datetime | None = Field(
        default=None,
        sa_column=Column(DateTime, server_default=func.now()),
    )

The adapter automatically builds three schemas:

Schema Fields Used in
public id, title, content, published, created_at GET responses
create title, content, published POST body
update title?, content?, published? PATCH body (all optional)

id is excluded from create/update — it is generated by the database. created_at is also excluded — it has a server_default, so the database sets it automatically.

Controlling the field sets

There are two ways to control which fields end up in the generated schemas, and you can use either:

  • On the model, via a crud_config attribute — the default for every collection built on that model (below).
  • Per collection, via constructor params — overrides the model's config for that one collection (see Passing fields at instantiation time).

crud_config on the model

To use a different set of fields, add a crud_config attribute directly to the model. The adapter picks it up automatically.

from typing import ClassVar
from fastapi_crud_generator.config import CRUDConfigDict

class Article(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    title: str
    content: str
    summary: str | None = None
    internal_notes: str | None = None  # should not appear in the API

    crud_config: ClassVar[CRUDConfigDict] = CRUDConfigDict(
        public_fields={"id", "title", "summary"},
        create_fields={"title", "content", "summary"},
        update_fields={"title", "content", "summary"},
    )

Every built-in adapter reads crud_config off the model, so this works the same on SQLModel, SQLAlchemy and Tortoise. Only how you attach it differs:

  • SQLModel — annotate it ClassVar, otherwise the model treats it as a table column (as shown above).
  • SQLAlchemy / Tortoise — a plain class attribute is enough; neither turns an unmapped attribute into a column.
# SQLAlchemy (DeclarativeBase)
class Article(Base):
    __tablename__ = "article"
    # ... mapped columns ...
    crud_config = CRUDConfigDict(create_fields={"title", "content"})

# Tortoise
class Article(Model):
    # ... fields ...
    crud_config = CRUDConfigDict(create_fields={"title", "content"})

base_fields — shared fields across schemas

If several schemas share a common set of fields, put them in base_fields:

crud_config: ClassVar[CRUDConfigDict] = CRUDConfigDict(
    base_fields={"title", "content"},
    public_fields={"id", "published"},    # → id, published, title, content
    create_fields={"category_id"},        # → category_id, title, content
)

base_fields is merged into each explicitly defined set.

Using a hand-written schema

Any schema can be replaced with a custom Pydantic model. The rest continue to be generated automatically.

class ArticleCreate(BaseModel):
    title: str
    content: str
    author_id: int  # a field that is not on the ORM model

crud = CRUDCollection(
    orm_adapter=adapter,
    create_schema=ArticleCreate,  # only this schema is custom
)

Passing fields at instantiation time

Field sets can also be passed directly to CRUDCollection, without touching the model:

crud = CRUDCollection(
    orm_adapter=adapter,
    public_fields={"id", "title"},
    create_fields={"title", "content"},
)

This is useful when the same model is used in multiple CRUDCollection instances with different field sets.