Schemas API#
fastapi_restly.schemas implements the Pydantic schema layer: schema base
classes, the ReadOnly and WriteOnly field markers, relationship
reference types, and schema generation from SQLAlchemy models.
See also
Work with Foreign Keys and Relationships covers MustExist checked foreign
keys and the IDRef / IDSchema relationship wire contract.
Custom Schemas and Field Types describes schema bases, field markers, and
aliases in prose.
- fastapi_restly.schemas.ReadOnly#
Marks a field as excluded from generated create and update schemas. The field remains available in responses.
- fastapi_restly.schemas.WriteOnly#
Marks a field as input-only by setting Pydantic’s serialization exclusion. See JSON document columns for restrictions inside JSON documents.
- class fastapi_restly.schemas.BaseSchema#
Bases:
BaseModelThin Pydantic base for ORM-facing Restly schemas.
Equivalent to:
class BaseSchema(pydantic.BaseModel): model_config = pydantic.ConfigDict(from_attributes=True)
from_attributes=Truelets Pydantic/FastAPI validate objects by attribute when the schema is used directly. The inherited CRUD endpoint methods still serialize throughto_single_response()so Restly-specific behavior such asWriteOnlyfiltering and relationship-id normalization is applied.- model_config: ClassVar[ConfigDict] = {'from_attributes': True}#
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class fastapi_restly.schemas.IDRef(value: ~typing.Any = <object object>, *, id: ~typing.Annotated[~typing.Any, fr.ReadOnly])#
Bases:
IDSchema,Generic[SQLAlchemyModel]Flat-id reference to a related row, for a RELATIONSHIP-named field.
Use
IDRef[T]when the field is named after a relationship (author: IDRef[User],products: list[IDRef[Product]]), not after a*_idcolumn. The wire format is the raw id (5); input also accepts{"id": N}. Restly resolves the id to the related ORM object, sodata.authoris anIDRefwrapper (read.id), not a plain scalar.For a scalar foreign-key COLUMN (
author_id,task_id) usefr.MustExist[int, T]instead – it keeps the field a plain checked id (data.author_id == 1), rather than making a*_idfield an object wrapper. For nested{"id": N}wire on a relationship, useIDSchema[T].author: IDRef[User] # to-one relationship, flat id products: list[IDRef[Product]] # serializes as [1, 2, 3]
Resolution applies the referenced model’s
default_scope: a reference to a row the scope hides raisesNotFound, so a tenant scope declared once covers every reference to the model. A model without adefault_scoperesolves by bare primary key; gate visibility there inauthorize(data.<field>.idis the requested id, before resolution) orbefore_action_commit(the resolved row is on the built object). See the Foreign Keys and Relationships how-to, “Visibility and Multi-Tenancy”.- model_config: ClassVar[ConfigDict] = {'from_attributes': True}#
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class fastapi_restly.schemas.IDSchema(*, id: Annotated[Any, fr.ReadOnly])#
Bases:
BaseSchema,Generic[SQLAlchemyModel]Response-schema base that adds a read-only
id; parametrized as a field type, a nested-object relationship reference.As a BASE CLASS (
class UserSchema(IDSchema): ...) it adds the resource’s own read-onlyid– the common use.As a FIELD TYPE on a RELATIONSHIP-named field (
author: IDSchema[User]) the wire format is{"id": N}(JSON-API / React-Admin); Restly resolves the id to the related ORM object, sodata.authoris a wrapper (read.id). UseIDRef[User]for flat-id wire (5) instead.
For a scalar foreign-key COLUMN (
author_id), usefr.MustExist[int, T]rather thanIDSchema/IDRef: it keeps the field a plain checked id instead of turning a*_idfield into an object wrapper.- get_sql_model_annotation() type[SQLAlchemyModel] | None#
Return the annotation on IDSchema when used as:
foo: IDSchema[Foo]
This property will return “Foo”.
- id: ReadOnly, FieldInfo(annotation=NoneType, required=True, json_schema_extra={'readOnly': True})]#
- model_config: ClassVar[ConfigDict] = {'from_attributes': True}#
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class fastapi_restly.schemas.MustExist#
Bases:
objectExistence-checked scalar foreign key.
MustExist[int]is a checkedintforeign key; the target model is inferred from the column’sForeignKey. Spell the model out with a second argument when you want it explicit (MustExist[int, Post]), and use the first for a non-int primary key (MustExist[UUID, Account]).Unlike
IDRef/IDSchemathis is not a wrapper: the field stays the pk scalar everywhere (wire, column,data.<field>), plus a batched existence check on write (404 on a miss). The check applies the target model’sdefault_scope; check against another clause, or none, by writing the marker formAnnotated[<pk>, RefExists(Model, scope=...)]directly (seeRefExists).
- class fastapi_restly.schemas.RefExists(model: type[~typing.Any] | type[~fastapi_restly.schemas._base._Infer], scope: ~fastapi_restly.clauses._runtime.WhereClause | ~fastapi_restly.clauses._runtime.Unscoped | ~fastapi_restly.schemas._base._NotGiven = <fastapi_restly.schemas._base._NotGiven object>)#
Bases:
objectMarker for an existence-checked scalar foreign key.
Built by
MustExist[pk]/MustExist[pk, Model](or written directly asAnnotated[pk, RefExists(Model)]). On write, Restly batch-checks that each marked id exists in the target table and raisesNotFound(404) on a miss. The field stays a plain scalar – this only adds the check; it does not wrap the value or do any relationship routing.The check applies the target model’s
default_scope, likeIDRef/IDSchemaresolution: a reference to a row the scope hides is a miss.scopeoverrides that per field: aWhereClausechecks against that clause instead (RefExists(Item, scope=ItemClauses.trashed)for a restore target), andscope=fr.clauses.UNSCOPEDchecks unscoped, greppably;Noneis rejected. A model without adefault_scopeis checked unscoped, as before.modelis the target ORM model, or_Inferwhen it should be resolved from the marked column’sForeignKey(theMustExist[pk]form).
- class fastapi_restly.schemas.TimestampsSchemaMixin(*, created_at: Annotated[datetime, fr.ReadOnly], updated_at: Annotated[datetime, fr.ReadOnly])#
Bases:
BaseModel- created_at: ReadOnly, FieldInfo(annotation=NoneType, required=True, json_schema_extra={'readOnly': True})]#
- model_config: ClassVar[ConfigDict] = {}#
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- updated_at: ReadOnly, FieldInfo(annotation=NoneType, required=True, json_schema_extra={'readOnly': True})]#
- fastapi_restly.schemas.derive_schema(model: type[Any], *, name: str | None = None) type[BaseSchema]#
Generate a Pydantic schema from a SQLAlchemy model.
This is the same schema that a view generates when it has no
schema: one field per column, and no relationship fields. A foreign key column, such asauthor_id, is an ordinary field. The fieldsid,created_atandupdated_at, and read-only columns such as acolumn_property, are markedReadOnly.- Parameters:
model – The SQLAlchemy model class.
name – Name for the generated schema class. Defaults to the model name followed by
Schema, as inUserSchema.
- Returns:
A Pydantic schema class.
- fastapi_restly.schemas.derive_schema_list_response(schema_response: type[BaseModel], *, pagination: NumberedPagination | NoPagination | None = NumberedPagination(default_page_size=50, max_page_size=1000, max_page=None, page_query_param='page', page_size_query_param='page_size', envelope=PaginatedEnvelope)) type[BaseModel]#
Build the list response class of a view: the envelope of its pagination, filled with its response class, named
<Resource>ListResponse.GET /of a view returns this class, except on a react-admin view, which returns a plain list. A custom list route names it, so OpenAPI shows one type for both routes:UserResponse = fr.schemas.derive_schema_response(UserSchema) UserListResponse = fr.schemas.derive_schema_list_response( UserResponse, pagination=AppView.pagination ) class UserView(AppView): schema = UserSchema @fr.get("/inactive", response_model=UserListResponse) async def inactive(self, list_params): ...
Each call with the same response class and envelope returns the same class. Only the envelope of the pagination matters: two paginations with the same envelope give the same class. A view has no attribute for this class. To change it, change the view’s
schema_responseor the envelope of itspagination.- Parameters:
schema_response – The view’s response class, such as the one from
derive_schema_response().pagination – The view’s pagination.
Noneis a view without pagination, as on the view. The default is the default pagination.
- Returns:
The list response class.
- fastapi_restly.schemas.derive_schema_response(schema: type[_SchemaT]) type[_SchemaT]#
Build the response class of a view’s schema: the schema without its
WriteOnlyfields, named<Resource>Response.A view without its own
schema_responsegets this class. Restly builds it also when the schema has noWriteOnlyfields, so OpenAPI showsUserResponseand not the view’s schema.The class is a subclass of
schema. Each call with the same schema returns the same class, so a custom route can name the class that the view uses:UserResponse = fr.schemas.derive_schema_response(UserSchema) class UserView(fr.AsyncRestView): schema = UserSchema @fr.get("/me", response_model=UserResponse) async def me(self): ...
- Parameters:
schema – The view’s schema.
- Returns:
The response class.