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_configattribute — 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.