model_config and ConfigDict: every option worth knowing, what each one changes, and the settings a Sillo request or response model usually wants.
from pydantic import BaseModel, ConfigDict
class PostCreate(BaseModel): model_config = ConfigDict( extra="forbid", str_strip_whitespace=True, frozen=True, )
title: str body: strmodel_config replaces v1’s class Config. It is inherited and merged, so a
base class can set policy for a whole family of models.
Extra fields
Section titled “Extra fields”extra="ignore" # default — drop unknown keysextra="forbid" # reject themextra="allow" # keep them as attributesThe default silently discards anything you did not declare. That is forgiving,
and it hides typos: a client sending publish instead of published gets a
200 and no effect.
extra="forbid" on request models turns that into a clear error:
{"loc": ["body", "publish"], "msg": "Extra inputs are not permitted"}The trade-off is compatibility: a client sending a field your newer version
removed now fails. For an internal API, forbid. For a public one consumed by
clients you do not control, ignore is the kinder default.
extra="allow" keeps unknown keys as attributes. Occasionally right for a
passthrough payload; usually a sign the shape has not been decided.
Strings
Section titled “Strings”str_strip_whitespace=Truestr_to_lower=Truestr_to_upper=Truestr_min_length=1str_max_length=1000Applied to every str field in the model.
str_strip_whitespace=True is worth setting on almost every request model.
A title of " " passes a bare str annotation and is not a title;
stripping first means min_length=1 catches it.
Validation behaviour
Section titled “Validation behaviour”strict=True # no type coercion anywherevalidate_assignment=True # validate on attribute assignmentvalidate_default=True # validate defaults toorevalidate_instances="always"strict=True is right for a JSON body, where the client controls the types. It
is wrong for anything derived from a URL or a form, which is strings by
definition. See Types.
validate_assignment=True closes the gap where a model is valid at
construction and invalid a line later:
post.title = 123 # raises, with this onvalidate_default=True catches a default that does not satisfy its own
constraints. A mistake that otherwise only surfaces when someone omits the
field.
Immutability
Section titled “Immutability”frozen=TrueNo assignment after construction, and the model becomes hashable.
A good default for request models: nothing downstream should be editing what the client sent, and a frozen model makes that structural rather than a convention.
Aliases
Section titled “Aliases”from pydantic.alias_generators import to_camel
model_config = ConfigDict( alias_generator=to_camel, populate_by_name=True, serialize_by_alias=True,)Every field becomes camelCase on the wire and stays snake_case in Python.
populate_by_name=True accepts either spelling on input;
serialize_by_alias=True emits the alias without needing by_alias=True at
each call site.
This is the tidy way to serve a JavaScript client without writing Python that looks like JavaScript. See Fields.
From attributes
Section titled “From attributes”from_attributes=TrueLets model_validate() read attributes instead of dict keys, which is what
makes an ORM instance validate directly:
PostOut.model_validate(post)v1 called this orm_mode. Set it on every output model that is built from a
Record model, and remember to
fetch the relations first.
Schema metadata
Section titled “Schema metadata”model_config = ConfigDict( title="Create a post", json_schema_extra={ "examples": [{"title": "Async Python", "body": "…"}], },)Both flow into the OpenAPI document. A worked example at the model level is the single most useful thing you can add to an API reference, more useful than per-field descriptions, because it shows a whole valid request.
Arbitrary types
Section titled “Arbitrary types”arbitrary_types_allowed=TruePermits a field annotated with a class Pydantic knows nothing about. It is then
validated only by isinstance, and has no JSON schema, so a model using it
cannot be a request or response model.
Fine for an internal model that never crosses the wire; a dead end for anything in your API.
Populating and protecting names
Section titled “Populating and protecting names”protected_namespaces=("model_",) # the defaultPydantic warns when a field name collides with its own model_* methods. If
your domain genuinely has a model_number, widen it:
model_config = ConfigDict(protected_namespaces=())Defaults worth adopting
Section titled “Defaults worth adopting”For a request model:
model_config = ConfigDict( extra="forbid", str_strip_whitespace=True, frozen=True, validate_default=True,)Reject typos, normalise whitespace, prevent downstream mutation, and catch a bad default at import time rather than at request time.
For a response model built from the ORM:
model_config = ConfigDict( from_attributes=True, validate_assignment=True,)Set them once on a base and inherit:
class RequestModel(BaseModel): model_config = ConfigDict(extra="forbid", str_strip_whitespace=True, frozen=True)
class PostCreate(RequestModel): title: str body: strWhich also gives you one place to change the policy when you decide forbid
was too strict.
Reading it back
Section titled “Reading it back”PostCreate.model_configA plain dict, merged across the inheritance chain, useful when a base class several levels up is setting something surprising.