Schemas
Schemas describe and validate incoming payloads. They are thin wrappers around Pydantic models with an additional async validation layer powered by fast-validation.
Generating a schema
Scaffold a schema with the CLI. Naming convention mirrors controllers and routes.
fast-app make schema Lead
The generator creates app/http_files/schemas/lead.py containing a LeadSchema stub:
from fast_app import Schema
from pydantic import Field
class LeadSchema(Schema):
name: str = Field(..., max_length=120)
Keep schemas under app/http_files/schemas/ so controllers and request handlers can import them easily.
Pydantic-powered validation
Schemas inherit from fast_validation.Schema, a Pydantic BaseModel with sensible defaults (whitespace stripping, assignment validation, enum support). You can use the full range of Pydantic field types, constraints, and validators.
- Required vs optional fields (
Field(..., description="...")vsField(None)) - Type hints for
EmailStr,constr,Literal,Annotated, etc. - Custom validators and field constraints, just like any Pydantic model.
Reference the Pydantic documentation for exhaustive options: docs.pydantic.dev.
from pydantic import Field, EmailStr, constr
class InviteSchema(Schema):
email: EmailStr
name: constr(min_length=2, max_length=40)
role: Literal["admin", "editor", "viewer"] = Field(..., description="Role in the workspace")
Convenience field types
When I need quick coercion helpers, fast_app.core.pydantic_types ships a handful of annotated types that feel native to Pydantic while keeping Mongo- and JSON-friendly behaviour:
ObjectIdField: Accepts strings orObjectIdinstances and serialises back to hex strings.DateFieldandDateTimeField: Parse ISO strings (and datetimes withZ) into Pythondate/datetime, serialising back to ISO format.IntFromStrField: Coerces numeric strings and exact floats to integers, rejecting booleans and fractional floats.JSONField: Allows raw dict/list input or JSON strings and returns Python objects.ShortStr: Strips whitespace and constrains length to 1–255 characters.
from fast_app.core.pydantic_types import (
ObjectIdField,
DateField,
DateTimeField,
IntFromStrField,
ShortStr,
)
class SubscriptionSchema(Schema):
user_id: ObjectIdField
plan: ShortStr
quota: IntFromStrField = 0
starts_at: DateField
renewed_at: DateTimeField | None = None
All of these types are plain Pydantic annotations, so they work with validators, JSON schema generation, and the automatic serialisation behaviour provided by FastApp resources.
Async rule validation
After Pydantic validates structure and types, you can run additional async checks (e.g., hit the database, inspect related models). Define rules inside an inner Meta class using Schema.Rule.
from typing import Optional, Literal
from pydantic import Field, EmailStr
from fast_app import Schema
from fast_app.core.pydantic_types import ObjectIdField
from fast_app.core.validation_rules.exists_validator_rule import ExistsValidatorRule
from app.models import Rep, County
class LeadSchema(Schema):
name: Optional[str] = Field(None, description="Lead's full name")
email: Optional[EmailStr] = Field(None, description="Lead's email")
rep_id: Optional[ObjectIdField] = Field(None, description="Assigned sales representative")
county_id: Optional[ObjectIdField] = Field(None, description="Lead county")
source: Optional[Literal["direct", "eshop", "pharmacy", "distributor"]] = None
class Meta:
rules = [
Schema.Rule("$.rep_id", [ExistsValidatorRule(allow_null=True)]),
Schema.Rule("$.county_id", [ExistsValidatorRule(allow_null=True)]),
]
Schema.Rule accepts a JSONPath-like selector and a list of validator rules. Each rule receives the value, the whole payload, and a location tuple.
Run rule validation explicitly:
payload = LeadSchema(**request.json)
await payload.validate() # raises ValidationRuleException on failure
Framework helpers such as fast_app.core.api.validate_request call await schema.validate() for you, so most controllers only need to call the helper.
Built-in rules
ExistsValidatorRule(model=None, *, field=None, db_key="_id", allow_null=False, is_object_id=True, each=False)— verifies that IDs exist in the database. Ifmodelis omitted, the rule infers the model from the field name (e.g.,rep_id→Rep). Wheneach=True, every value in a list is checked.
To write your own rule, subclass fast_validation.validation_rule.ValidatorRule:
from fast_validation import ValidatorRule, ValidationRuleException
class LongitudeRule(ValidatorRule):
async def validate(self, *, value, data, loc):
if value is None:
return
min_value, max_value = await make_db_call()
if not (min_value <= float(value) <= max_value):
raise ValidationRuleException("Longitude must be between -180 and 180", loc=tuple(loc))
class LocationSchema(Schema):
longitude: float | None = None
class Meta:
rules = [Schema.Rule("$.longitude", [LongitudeRule()])]
Rules run sequentially for each matched value; collect errors are aggregated into a single ValidationRuleException.
Tips
- Use
Field(..., alias="externalName")when accepting camelCase payloads. - Set
Field(default_factory=list)for mutable defaults like tags or settings. - Mark partial update schemas with optional fields and call
await schema.validate(partial=True)to skip unset values. - Keep schema modules small and reusable; share nested schemas (e.g.,
LeadScheduleSchema) across multiple resources.
With schemas, you get strict structural validation from Pydantic plus async rule checks for cross-cutting invariants.