Technical Details#
Schema Generation Under the Hood#
FastAPI-Restly builds request and response schemas from your declared schema class,
or auto-generates one from the SQLAlchemy model when schema is omitted on a
view.
ReadOnly and WriteOnly#
Field-level markers are implemented with typing.Annotated metadata:
class UserRead(IDSchema):
id: ReadOnly[int]
email: str
password: WriteOnly[str]
IDSchema is primarily a response-schema base: it is BaseSchema with a
read-only id field. IDRef[Model] and IDSchema[Model] are the model-aware
reference forms; their validators coerce the id value to match the SQLAlchemy
model’s actual primary-key type.
ReadOnly[...]fields are excluded from generated create/update input schemas.WriteOnly[...]fields are accepted on input and excluded from serialized responses. The filtering is done explicitly into_response_schema()(a method onAsyncRestView/RestView, defined on the internal abstract base they share), which skips any field where the internal write-only predicate returnsTrue. FastAPI’s response model serialization does not filter them; a custom serialization path that bypassesto_response_schema()would exposeWriteOnlyfields.
Generated Input Schemas#
For a view schema UserRead, Restly derives two input schemas in
before_include_view():
creation_schema: produced bycreate_model_without_read_only_fields(), which creates a subclass mixing inOmitReadOnlyMixinbeforeUserReadin the MRO.OmitReadOnlyMixin.__pydantic_init_subclass__directly deletesReadOnlyentries fromcls.model_fieldsand callsmodel_rebuild(force=True). The subclass still inherits validators fromUserReadfor the fields that remain.update_schema: produced bycreate_model_with_optional_fields(), which mixes in bothPatchMixinandOmitReadOnlyMixin. AfterOmitReadOnlyMixinstrips the read-only fields,PatchMixin.__pydantic_init_subclass__setsfield.default = Noneand wraps every remaining annotation inOptional[...]. Original field defaults fromUserReadare replaced byNone, not preserved.
The generated class names use resource-first role suffixes. UserRead derives
UserCreate and UserUpdate. The Read suffix is the only suffix Restly
strips when deriving request-schema names; other schema names are kept literally,
so UserSchema derives UserSchemaCreate and UserSchemaUpdate. When schema
is omitted entirely, a model named User auto-generates UserRead as the
response schema.
Both derived schemas are stored as class attributes on the view and are frozen
at registration time (see List Parameters Lifecycle).
They can be overridden by declaring creation_schema or update_schema directly
on the view class before include_view() is called.
Auto-Generated Schemas#
create_schema_from_model(model_cls, ...) walks all Mapped[...] annotations
on the model (including inherited ones) and builds a Pydantic schema. Key
behaviours:
Base class selection: The function checks whether the model has fields named
id,created_at, andupdated_atto decide which schema base classes to mix in (IDSchema,TimestampsSchemaMixin,BaseSchema). It does not inspect the model’s Python inheritance hierarchy; a model with a field accidentally namedidwill receiveIDSchemaas a base.ReadOnly annotation: Only three field names are automatically marked
ReadOnly:"id","created_at", and"updated_at"(controlled byinclude_readonly_fields=True). Any other server-side default or auto-populated column will not be markedReadOnlyby auto-generation.Relationship fields: Included when
include_relationships=True(the default forcreate_schema_from_model). Relationship fields are set toOptionalwithdefault=Nonein the generated schema and nested schemas are generated recursively (one level deep, without relationships, to avoid circular references).
When a RestView / AsyncRestView omits schema, the internal view setup calls
create_schema_from_model(model_cls, schema_name=schema_name, include_relationships=False).
It does not apply any other filtering beyond excluding relationship attributes;
foreign-key columns appear in the generated schema as ordinary scalar fields.
SQLAlchemy-to-Pydantic Type Mapping#
convert_sqlalchemy_type_to_pydantic maps the Python type extracted from each
Mapped[T] annotation to its Pydantic equivalent. Pass-through types (those
already understood by Pydantic) are returned unchanged:
SQLAlchemy / Python annotation |
Pydantic field type |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
same enum subclass |
SQLAlchemy |
|
SQLAlchemy |
|
SQLAlchemy |
|
SQLAlchemy |
|
SQLAlchemy |
|
SQLAlchemy |
|
SQLAlchemy |
|
Any type not in this table raises TypeError at schema-generation time. For
custom column types, declare an explicit schema and bypass auto-generation.
View Classes and Registration#
AsyncRestView and RestView#
Both AsyncRestView (async) and RestView (sync) are public API and
share the same CRUD structure via an internal abstract base class (not exposed
as fr.*). The choice between them is determined by which class you subclass —
AsyncRestView declares session: AsyncSessionDep and RestView declares
session: SessionDep. A subclass can override that session annotation with
its own Annotated[..., Depends(...)] dependency for per-view session wiring.
The async and sync variants have identical endpoint signatures; the only
difference is that the async variant uses await in its process methods.
AsyncSessionDep and SessionDep use Restly’s built-in session generators.
Those generators yield a SQLAlchemy session to the endpoint and, by default,
commit when the endpoint successfully produces a response. On FastAPI versions
where Depends(..., scope="function") exists, Restly requests that scope so
the commit runs before FastAPI sends the response. On older FastAPI versions,
cleanup timing follows FastAPI’s default yield dependency behavior and may
run after the response has already been sent. Set
commit_session_on_response=False in fr.configure(...) to disable the
built-in commit and manage transactions explicitly. Custom session generators
configured with session_generator or sync_session_generator are passed
through unchanged; their generator body owns transaction handling.
Both views expose several class variables that affect endpoint registration and runtime behaviour:
schema— the Pydantic schema class; auto-generated if absent.creation_schema,update_schema— derived fromschemaif not declared.model— the SQLAlchemy model class.id_type— Python type for the scalar{id}path parameter (defaultint). Composite primary keys are outside the generated CRUD route contract; useViewdirectly when a resource needs a multi-part identity.exclude_routes— iterable of route names to suppress (e.g.exclude_routes = [fr.ViewRoute.DELETE]). Route-name strings such as"delete"are also accepted. Routes listed here have their_api_route_argsmarker removed duringbefore_include_view()so FastAPI never registers them.include_pagination_metadata— ifTrue, thelistingendpoint returns a paginated envelope withitems,total,page,page_size, andtotal_pages.
include_view()#
include_view() is the registration boundary between declarative view modules
and application composition. For larger apps, define view classes without
side effects in feature modules, then include them from the module that builds
your FastAPI app or APIRouter:
fr.include_view(app, MyView)
This keeps imports predictable: importing myapp.users.views defines
UserView, while myapp.main or myapp.users.router decides which app/router
receives it. For small apps and examples, include_view() also works as a
decorator:
@fr.include_view(app)
class MyView(fr.AsyncRestView):
...
Both forms call before_include_view() (which generates derived schemas,
annotates endpoint signatures, and registers the listing_param_schema), then
attach an APIRouter to the parent app/router.
Endpoint / Handler Separation#
Every CRUD endpoint delegates to a perform_* handler (perform_listing,
perform_get, perform_create, perform_update, perform_delete). Override
the perform_* handler to change business logic while keeping the endpoint
wrapper intact, or override the endpoint method itself (e.g. listing) to replace
the full request/response flow.
Nested Response Schemas vs Write Payloads#
Nested schemas serve two different roles in Restly today:
Response serialization: supported. The CRUD views recursively build
selectinload(...)options for nested relationship fields in the response schema, so related objects can be serialized efficiently and with aliases.Create/update payloads: not supported in the general case. The default
build_from_schema()/apply_schema()flow expects payload keys to map directly to model attributes, with*_id: IDRef[Model]as the usual special case for foreign keys. When anIDRef/IDSchemareference has been resolved to an ORM object, the helpers inspect the SQLAlchemy mapper and dataclass constructor fields so FK-first (author_idinit-enabled) and relationship-first (authorinit-enabled) declarations both work. For one resolved reference, Restly may pass the FK scalar, the relationship object, or both when both dataclass fields are required; both values are derived from the same row. If the client explicitly supplies both fields, Restly validates that they refer to the same row before construction/update. Explicitnullis treated as an intentional “no row” value for that consistency check; omitted optional fields are ignored.
If you declare a nested input field like address: AddressRead on a write
schema, the default CRUD implementation will pass that nested Pydantic object
through to the SQLAlchemy model constructor or attribute setter, which usually
does not match the ORM model shape. Use a flattened schema or override
perform_create() / perform_update() to transform the payload first.
List Parameters Lifecycle#
List endpoints accept URL query parameters of the form
name=John, age__gte=18, sort=-created_at, and
page=2&page_size=50. The full operator surface (__ne, __isnull,
__contains, __icontains, …) is documented in
the how-to guide.
During before_include_view(), the framework freezes a single class-level
attribute:
cls.listing_param_schema— the query-parameter Pydantic schema generated bycreate_list_params_schema(cls.schema, default_page_size=..., max_page_size=...). The schema covers pagination, sorting, and one filter parameter per response-schema field with optional operator suffixes. It is generated once per registration and never re-derived.
Custom dialects (e.g. react-admin’s
AsyncReactAdminView / ReactAdminView) live as
parallel view classes that bypass apply_list_params entirely and
implement their own request/response contract.
Restly Runtime Configuration#
Restly exposes one public process-wide runtime configuration. Most applications configure it once during startup:
fr.configure(async_database_url="sqlite+aiosqlite:///app.db")
fr.configure(...) rejects no-op calls. Pass at least one setup option: an app
for default exception-handler registration, a database URL, an engine, a session
maker, a custom session generator, or an explicit
commit_session_on_response policy.
Internally, Restly keeps a private context object so its own tests and fixtures can isolate runtime state. That context is not a public multi-engine feature. If an application needs multiple databases, wire a custom FastAPI dependency or session generator for that view. Restly does not currently bind different views to different named contexts.
Session Factory Defaults#
When fr.configure() creates session factories from URLs or engines, Restly
sets a few SQLAlchemy session options intentionally:
Factory |
Autoflush |
Expire on commit |
|---|---|---|
Async |
|
|
Sync |
SQLAlchemy default ( |
|
expire_on_commit=False is used for both sync and async sessions so ORM
objects remain readable after a route commits. FastAPI response serialization
and Restly’s response-schema conversion read attributes from ORM objects after
the write path has flushed and refreshed them. If commit expired those
attributes, serialization could trigger implicit database reads. In async code
that can fail outside an awaited SQLAlchemy call; in sync code it makes response
rendering unexpectedly database-dependent.
The autoflush setting is intentionally different. Async sessions disable autoflush because autoflush can turn a read operation into an implicit write and database I/O must happen at explicit awaited SQLAlchemy boundaries. Restly’s async CRUD helpers flush explicitly when writes should hit the database. Sync sessions keep SQLAlchemy’s default autoflush behavior, preserving the usual unit-of-work ergonomics where ORM queries see pending in-session changes.
Projects that provide custom sessionmakers or session generators should preserve these assumptions unless they deliberately want different behavior.
See Also#
How-To: Filter, Sort, and Paginate Lists — full filter, sort, and pagination reference.