Models (Pydantic)¶
FastAPI validates request data and serializes responses with Pydantic v2 models. The binary embeds its own
implementation of Pydantic v2's lax mode, with pydantic-core's error types, messages, locations and contexts, so
that a 422 is the same byte for byte. It also covers validators, model_config, computed fields,
TypeAdapter and pydantic-settings.
A Pydantic v2 model with EmailStr, Literal, nested models, Decimal constraints, an alias with
populate_by_name, str_strip_whitespace, a field validator, a model validator and a computed field; then
model_dump with its options. Like every example on this site, it is compiled and compared with FastAPI in CI (how).
from decimal import Decimal
from typing import Literal
from fastapi import APIRouter
from pydantic import BaseModel, ConfigDict, EmailStr, Field, computed_field, field_validator, model_validator
router = APIRouter(prefix="/models", tags=["models"])
class Address(BaseModel):
city: str
country: str = Field(min_length=2, max_length=2, description="ISO 3166 code")
class Customer(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True, populate_by_name=True)
email: EmailStr
name: str = Field(min_length=1, max_length=40)
plan: Literal["free", "pro"] = "free"
tags: list[str] = []
addresses: list[Address] = []
credit: Decimal = Field(default=Decimal("0"), max_digits=8, decimal_places=2)
referrer_id: int | None = Field(default=None, alias="referrerId")
@field_validator("tags")
@classmethod
def normalize_tags(cls, tags: list[str]) -> list[str]:
return sorted({tag.lower() for tag in tags})
@model_validator(mode="after")
def pro_needs_an_address(self):
if self.plan == "pro" and not self.addresses:
raise ValueError("a pro customer needs an address")
return self
@computed_field
@property
def display_name(self) -> str:
return f"{self.name} <{self.email}>"
@router.post("/customers")
async def create_customer(customer: Customer) -> Customer:
return customer
@router.post("/customers/dump")
async def dump_customer(customer: Customer):
return {
"python": customer.model_dump(exclude={"addresses"}),
"json": customer.model_dump(mode="json", by_alias=True, exclude_none=True),
"fields_set": sorted(customer.model_fields_set),
}
What is native¶
- Lax-mode validation with pydantic-core's error types, messages, locations and contexts (speedate for
dates), smart unions (exactness, fields set), enums, literals, nested models, lists/sets/tuples/dicts,
Optional,Fieldconstraints (and pydantic 1'smin_items/max_items, whichField()still maps tomin_length/max_length), aliases (below), defaults anddefault_factory,validate_assignment(an assignment runs the field'sbeforevalidators, its type, itsaftervalidators withinfo.dataholding every other field, then the model'saftervalidators: one of those that raises leaves the value assigned, error at loc(), as pydantic-core does; refused with amode="before"model validator; anaftervalidator's error reports the raw input; amode="before"field validator defined after amode="after"one of the same field is refused),extra=,from_attributes,str_strip_whitespace/to_lower/to_upper,use_enum_values,model_configas a dict orConfigDict, v1class Config(v1-only keys ignored like Pydantic v2 does). Thestr_*settings also apply to the string anEmailStrvalidates. Refused: aIterable[T],MutableSequence[T],Collection[T]as a field, parameter,TypeAdapteror response type (pydantic validates anIterablelazily, a one-shot iterator whose item errors come at iteration: annotatelist[...]; theAsyncIterable[T]/Iterable[T]return annotation of a generator endpoint is its stream item type, see Responses). - An Enum member given to a scalar field (an ORM enum column read into
status: str...) as pydantic-core takes it: astr/intsubclass member (class X(str, Enum),StrEnum,IntEnum) is its value forstr,int,float,boolandLiteral(anintone isstr(value)in astr), a plain member isstr(value)in astrand its value, unchecked, in an unconstrainedint. uuid.UUIDfields and parameters: a UUID instance, a str in the simple, hyphenated,{braced}orurn:uuid:form, or bytes, with pydantic-core'suuid_type/uuid_parsingerrors (the messages of theuuidcrate pinned by the project's pydantic-core); dumped as the hyphenated str.@field_validator/@validator(after and before, including_x = field_validator(...)(lambda v: ...)),info: ValidationInfoin after validators (info.data: the earlier fields that passed, after their own validators, defaults included;info.field_name; other attributes, andinfoin before validators, are refused), v1values(pydantic's own signature rules:field/configparameters are refused),@model_validator(before/after); validators that raise produce the 422 at their place. Before validators run on the raw input in reverse definition order, like pydantic-core.model_dump(mode=, by_alias=, exclude_none=, exclude_unset=, exclude=, include=)(top-level field names forexclude/include),model_dump_json,model_validate(_json),model_copy(update=, deep=),model_fields_set,model_fields,ValidationError.errors()(URL with the major.minor of the pydantic the project locks, else the one installed next to py2axum;ctx.erroris the exception raised by the validator, rendered as its attributes byjsonable_encoder;include_*options) anderror_count().Model.model_json_schema()(default arguments only), computed at translation time like theGenerateJsonSchemaof pydantic 2.13 and 2.14 (identical on this subset) (key order included; same subset as MCP tool schemas, plusmodel_config extra=). The receiver must be a model class by name, orexpr.attrwhere every value the project binds to an attribute or keywordattris a model class (a tool registry); anything else is refused.model_config frozen=True(assignment raisesfrozen_instance; frozen models hash by value),Field(validate_default=True), field options given inAnnotated[T, Field(...)].- Aliases:
alias=,validation_alias=(a key,AliasPath,AliasChoices: the first path found wins, a missing field is reported at the first one, the input keys a field did not consume are its extras) andserialization_alias=apart;validate_by_alias/validate_by_name(populate_by_name),serialize_by_alias(each model's own setting whenby_alias=is not given).alias_generator=:pydantic.alias_generators(to_camel,to_pascal,to_snake), a lambda or a one-returnfunction of the name made of string operations, or anAliasGenerator(alias=, validation_alias=, serialization_alias=)of those, evaluated at translation time on the field names (a field that declares an alias keeps it, the generator fills the others, as pydantic does); anything else is refused. Field(exclude=True)(never dumped),repr=False,frozen=True(an assignment raisesfrozen_field),multiple_of=(onint/float, checked before the bounds; a float within|x| / 1e9, like pydantic-core),union_mode="left_to_right",discriminator="field"(a discriminated union, possibly nested in another one: the tag picks the member,union_tag_not_found/union_tag_invalidotherwise; stringLiteraltags only;Discriminator(fn)with members taggedAnnotated[T, Tag('x')], the function called on the input, and itscustom_error_type/_message/_contextof strings),model_config str_min_length=/str_max_length=,StringConstraints(ascii_only=True)(pydantic 2.14). A str is checked as pydantic-core does: stripped, thenascii_only, the lengths and the pattern, and only then lowered or uppercased (str_to_upperwith a lowercasepatternaccepts lowercase input). Thestr_*anduse_enum_valuessettings of a model apply to the items and keys of its containers too.- Strict mode (
model_config strict=True,Field(strict=)), with pydantic-core's python-mode rules (FastAPI validates a decoded JSON body as Python data): only the exact type passes, with the type's own error (int_type,date_type,tuple_type,is_instance_offorDecimal/UUID/enums...); anintis a validfloat. A model's strict config covers its fields' items, not the fields of another model it holds;Field(strict=True)covers the field's own type (a list's items keep the model's mode). Refused:Field(strict=True)on a union,Field(strict=False)on a container in a strict model. A strict URL refuses what theurlcrate reports as a syntax violation (url_syntax_violation), as pydantic-core does. Field(default_factory=lambda data: ...): called with the fields validated before it; after an error of an earlier field it is not called (default_factory_not_called; before pydantic 2.14 amissingfield does not count: the factory is called with what was validated, and what it raises is a 500).- Validators in a field's annotation (
Annotated[T, AfterValidator(f)],BeforeValidator,PlainValidator,WrapValidator, directly or through a type alias), run like@field_validators defined before the class's: a function, a lambda,info(data,field_name,context:None) in an after validator.@field_validator(mode="plain")andPlainValidatorreplace the type (and the validators before them);mode="wrap"andWrapValidatorget ahandlerthat validates against the type (aValidationErrorwith locations from the value, titleValidatorCallable; re-raised, its errors land under the field). Refused: a wrap validator after other validators of the field, anything butAfterValidatorafter aWrapValidatorin the annotation, a validator of an item inside a container. PydanticCustomError(type, template, context)raised by a validator: its own error (the template filled from the context: a str as is, a bool as an int, other values byrepr);PydanticUseDefaultraised by a before or wrap validator: the field's default (still inmodel_fields_set; no default is pydantic-core's schema error, a 500).model_dump(context=)reaches the serializers'info.context.- Serializers:
@field_serializer(*fields, mode="plain"|"wrap", when_used=)(instance, class or static method,infowithmode,field_name,context:None),PlainSerializer/WrapSerializerin the annotation,@model_serializer(mode="plain"|"wrap"),Field(exclude_if=),model_dump(exclude_defaults=True)(a field with a@field_serializeris never left out by it, as in pydantic-core). Serializers run while dumping and must not await; their result is dumped by its value's type (the return annotation is not used). - Types:
tuple[A, B, C],tuple[()](missingper position,too_longalone),collections.deque[T],pydantic.SecretStr(shown**********instr,reprand JSON dumps,get_secret_value()),UUID1...UUID8(uuid_version),fractions.Fraction(CPython 3.12+'s grammar, exact from a float; dumped as itsstr()in both modes),complex(CPython'scomplex()grammar;repr()like CPython, JSON like pydantic-core:1e20is written100000000000000000000),Callable(callable()),SkipValidation[T](the value as given),types.EllipsisTypeandMISSINGwithout a default (no JSON value is one), a plain class of the project (arbitrary_types_allowed: anisinstancecheck; one defining__get_pydantic_core_schema__is refused), a type alias assigned after the class that uses it (MyInt = intunderfrom __future__ import annotations),RootModel[T]/RootModelwithroot: T(the input and the dump are the root value; validators, serializers and aliases on it are refused),frozenset[T],collections.Counter[K],collections.OrderedDict[K, V],collections.abc.Sequence[T](a str refused withsequence_str, a tuple stays a tuple),type[Model],type[BaseModel],type[PlainClass](is_subclass_of). A mutable default evaluated once (deque(), a model instance) is copied for each instance, like pydantic'ssmart_deepcopy. - Generic models (
class Response(BaseModel, Generic[T]),class Response[T](BaseModel), a subclass ofParent[T]): eachResponse[int]used is specialized at translation time, named like pydantic does (Response[int], in errors andtype(m).__name__); the generic class used as such validates a parameter as its default, else its bound, elseAny. Its arguments must mean the same in the model's module; TypeVar constraints are refused. - JSON dumps (responses,
model_dump(mode="json"),model_dump_json) write afloatfield holding an int (a default0) as a float, like pydantic-core's typed serializer;model_fields_setincludes the extras of anextra="allow"model. TypeAdapter(T):validate_python(ORM objects withfrom_attributes, iterables),validate_json,dump_python,dump_json, for types written in the source or known at run time.EmailStr(rule-by-rule port of email-validator, same messages, except IDNA encoding and NFC normalization of internationalized domains, which are accepted and lowercased),AnyUrl/AnyHttpUrl/HttpUrl/RedisDsn(parsed with theurlcrate like pydantic-core, type defaults, attributes, same errors).- pydantic-settings
BaseSettings: environment variables (not.envfiles),validate_default=True,env_prefix,case_sensitive(names compared case-insensitively by default, the last of two names that differ only in case wins),env_parse_none_str(an environment value equal to it, case included, isNone; init keywords are not parsed), JSON values for container/model fields (a union with such a member keeps the raw string when the JSON does not parse); otherenv_*options and_env_*init keywords are refused; a class body run like a script (class-levelif, attributes reading earlier ones) is evaluated once. decimal.Decimalfields: lax validation like pydantic-core (a float through itsrepr, a string throughDecimal(str), finite values only),max_digits/decimal_placeson the normalized value, thenle/lt/ge/gt(int or float literals); dumped as a string in JSON mode.@computed_field(bare, over@propertyor alone): serialized after the fields and the extras,exclude_noneapplies,exclude_unsetdoes not,repr()shows them. The property must not await.- A
@classmethodoverride ofmodel_validate(called by name,Model.model_validate(x)) callingsuper().model_validate(...)orsuper(Model, cls): the parent's (a project model's override, else BaseModel's),clsstill the class called.super(cls, cls)is accepted when no project model subclasses the class (else its target depends on the class called: refused). - A model's own
def __init__(self, **data)callingsuper().__init__(**data): run byModel(...); validation from attributes (from_attributes, ORM objects) or of an existing instance skips it, as in pydantic-core. - Private attributes (
_name: T = PrivateAttr(default=/default_factory=), any_name): per instance, never validated nor dumped; a_nameassigned on an instance is not dumped either (unlessextra="allow"). An instance of the response model's own class is serialized as returned, private attributes included.
The binary does not read .env files: see Environment variables.
- model_config val_json_bytes= / ser_json_bytes= ("utf8", "base64", "hex"): a str input of a
bytes field of the model (inside containers too) decoded as pydantic-core does (URL-safe base64, the
standard alphabet when the input holds + or /, padding optional; its error messages, which differ
between pydantic-core 2.46 and 2.50), the JSON dump encoded (URL-safe base64 with padding, lowercase hex).
A nested model uses its own config.
What stays in Python¶
- Not supported:
@computed_field(...)options, nestedexclude/includedicts,Iterable[T](validated lazily by pydantic),Json(parsed by jiter, whose error messages are not reproduced), classes with__get_pydantic_core_schema__,create_model,ImportString. - A union with a member that has validators (directly or in a nested model) is refused: Pydantic would try the next member when one raises.
Differences¶
- A plain Enum member whose value is not an integer, given to a constrained
int, reportsint_parsingwhere pydantic-core reportsint_parsing_size. - With
extra="allow", an extra key named like a computed field shadows it on attribute access and inmodel_dump_json(Pydantic writes both keys). jsonable_encoderof an integralDecimalbeyond 64 bits gives a float (Python: an int).- Validating a model that defines its own
__init__from a dict (request body,model_validate(dict), nested), where pydantic-core calls the__init__, raises a py2axum RuntimeError (500) instead. - A
Fractionorcomplexwritten with non-ASCII digits (CPython reads any Unicode decimal digit) is a 500. - Arithmetic on a
Fractionorcomplexvalue in the application's own code is not translated (a TypeError).