Pydantic¶
Generates a Pydantic model straight from a hare model — for API response schemas, without hand-writing a parallel Pydantic class for every model.
from hare.contrib.pydantic import pydantic_model_creator, pydantic_queryset_creator
AuthorSchema = pydantic_model_creator(Author)
AuthorListSchema = pydantic_queryset_creator(Author)
pydantic_model_creator()¶
def pydantic_model_creator(
model: type[Model],
*,
name: str | None = None,
exclude: tuple[str, ...] | None = None,
include: tuple[str, ...] | None = None,
computed: tuple[str, ...] | None = None,
extra_fields: dict[str, Any] | None = None,
related_values: dict[str, str] | None = None,
optional: tuple[str, ...] | None = None,
allow_cycles: bool | None = None,
sort_alphabetically: bool | None = None,
exclude_readonly: bool = False,
meta_override: type | None = None,
model_config: ConfigDict | None = None,
validators: dict[str, Any] | None = None,
module: str = "hare.contrib.pydantic",
exclude_sensitive: bool = False,
relations_as_ids: bool = False,
) -> type[PydanticModel]: ...
| Parameter | Meaning |
|---|---|
name |
Explicit model name, instead of an auto-generated one. |
exclude / include |
Extra fields to drop/keep, on top of whatever PydanticMeta already says. A path through a relation ("author.name") reaches a field of the related schema; in include it also keeps the relation itself, with only the listed fields. |
computed |
Names of @property/methods on the model to expose as computed fields; a path through a relation ("books.title_length") adds one to the related schema. |
extra_fields |
Values read from the row by name — a query's annotation, an attribute set on the row — with their types; see Extra fields and related values. |
related_values |
Fields of related rows, flattened — a name to the path of the related field; see Extra fields and related values. |
optional |
Fields to make optional in the schema even though they aren't nullable on the model. |
allow_cycles |
Allow self-referential/cyclic relations to recurse (default False — cycles are cut off). |
sort_alphabetically |
Sort fields alphabetically instead of declaration order. |
exclude_readonly |
A write schema without the fields the database or hare fills in — see exclude_readonly |
meta_override |
A PydanticMeta-shaped class to override the model's own, without editing the model — every option it sets, model_config included, replaces the model's. |
model_config |
A Pydantic ConfigDict merged into the generated model's config. |
validators |
{validator_name: field_validator(...)(func)}, passed through to pydantic.create_model's __validators__. |
module |
The __module__ of the generated class — "hare.contrib.pydantic" (PYDANTIC_MODELS_MODULE) by default. |
exclude_sensitive |
Drop every sensitive=True field (Model._meta.sensitive_fields — see Sensitive fields) — nested relation schemas included — see exclude_sensitive |
relations_as_ids |
Render every forward FK/O2O as its flat <name>_id field instead of a nested submodel — see Relations as ids. |
A name in exclude, include, optional or computed that is no field, relation or computed field of
the model — of the related model, for a path — raises ConfigurationError: a misspelt exclude would
leave the field it means in the schema.
exclude_readonly¶
Build a write-schema variant that drops every database-assigned field (a pk the database or ORM assigns —
generated, or with a default/db_default — and anything with generated=True — includes
auto_now/auto_now_add and a TSVectorField(stored=True)), plus every backward relation and computed
field (neither can ever be supplied on create). A forward FK/O2O/M2M stays in the schema — it isn't
database-assigned, the caller still has to supply it (required or optional, following the same
nullable/default rules as any other field). A natural primary key (a CharField(primary_key=True) or
CompositePrimaryKey components with no default) stays in the schema as a required field — the caller
has to supply it to create the row. An O2O primary key is represented by its relation (or its <name>_id
field with relations_as_ids=True).
exclude_sensitive¶
Drop every sensitive=True field (Model._meta.sensitive_fields — see Sensitive
fields) — nested relation schemas
included, and a related value (related_values) of such a field. Useful for a public/export schema.
Schemas built with and without it are cached separately and never mixed. The <name>_blind_index field
of an encrypted field with blind_index=True is in no schema, with or without it: hare writes it with
the field, and its HMAC is no data.
Generated models are cached and reused for identical (model, options) combinations — calling
pydantic_model_creator(Author) twice returns the same class.
A field's own ORM-level Field(validators=[...]) (see Validators) also runs
automatically in the generated schema — model_validate()/construction raises Pydantic's
ValidationError if any of them reject the value, in addition to (not instead of) whatever you pass via
the validators= parameter above for that same field. A field listed in optional= accepts an explicit
null (meaning "not provided") — its ORM validators don't run on it. The values a container holds
(ArrayField, MapField, TupleField), at any depth, are checked by their own fields as a write checks
them — an element longer than its CharField(max_length=5) or beyond its SmallIntField's range fails
with the same message (tags[0]: Length of 'abcdefg' 7 > 5).
Value types: a BinaryField is carried in JSON as base64 (model_dump_json() encodes it,
model_validate_json() decodes it, the schema says format: base64url), so arbitrary bytes round-trip;
pass your own ser_json_bytes/val_json_bytes in model_config to change that. A float JSON has no number
for — NaN or infinity of a FloatField, inside an ArrayField too — is written as its text ("NaN",
"Infinity", as an outbox event holds it) and read back by the schema (ser_json_inf_nan); so is the
infinite bound of a DecimalRangeField or DecimalMultiRangeField range. A JSONField
with a
declared field_type gets that type in the schema (Any without one). An IntEnumField/CharEnumField
is a $ref to the enum alone, without the column's numeric range or length constraints.
A field is optional in the generated schema if any of: it's listed in optional=, it's nullable on
the model, it has a Python-side default=, it has a db_default=, it's a non-PK field explicitly marked
generated=True (a GeneratedField, or any other field the database fills in — the caller shouldn't
have to provide it, and it's also marked readOnly in the JSON Schema), or it's a
DatetimeField/TimeField with auto_now=True/auto_now_add=True (the ORM fills it in on save, same
reasoning, also marked readOnly) — otherwise it's required. A PK is a separate case: an auto-increment
integer PK is generated=True too, but stays required in the schema like every other PK component,
regardless of this rule — unless it's listed in optional=, which makes it optional and nullable like
any other field.
A field optional only because the database or the ORM fills it — a db_default, a generated value, an
auto_now/auto_now_add stamp, the tenant — and refusing NULL on the model is left out of
model_dump() while it holds None, so Model.objects.create(**schema.model_dump()) leaves it to them
instead of writing a NULL the column refuses.
This also applies when fields use serialization aliases, including model_dump(by_alias=True) and
model_config["serialize_by_alias"]; nullable model fields still keep their explicit None.
A model with a CompositePrimaryKey works the same way
as one with a single-column pk — every component of the composite key becomes its own field in the
generated schema.
Meta.tenant_field is optional too: save()/create() fill it from the active tenant scope — when that
scope is one value; under several
values the input has to carry it, and it
is checked against the scope. That covers a tenant FK (tenant_field = "company_id" or "company" for
company = fields.ForeignKeyField(...)) as a nested submodel as well as its company_id id field.
A nested forward FK/O2O submodel is <Submodel> | None even for a non-null FK when the related model has
Meta.soft_delete_field, Meta.tenant_field or a custom Meta.manager filter — reading the relation
gives None for a target that scope hides, so the schema accepts it.
Relations as ids¶
By default a forward FK/O2O becomes a nested submodel of the related model. A JSON API usually wants the
flat id instead — relations_as_ids=True does that:
class Book(Model):
id = fields.IntField(primary_key=True)
title = fields.CharField(max_length=200)
author: fields.ForeignKeyRelation[Author] = fields.ForeignKeyField("models.Author", related_name="books")
editor: fields.ForeignKeyNullableRelation[Author] = fields.ForeignKeyField("models.Author", null=True)
tags: fields.ManyToManyRelation[Tag] = fields.ManyToManyField("models.Tag")
BookSchema = pydantic_model_creator(Book, relations_as_ids=True)
# fields: id, title, author_id (UUID, required), editor_id (UUID | None, optional), tags (list[Tag])
BookCreate = pydantic_model_creator(Book, relations_as_ids=True, exclude_readonly=True, exclude=("tags",))
- Field name is the model's own shadow attribute —
<name>_id(book.author_id), also when the FK sets a custom database column viasource_field=: the column name changes, the attribute doesn't. An FK to a model with aCompositePrimaryKeygets one field per component (<name>_<pk_component>). - Type is the target model's pk type (
int,UUID,strwith itsmax_length, …); nullability and required-ness follow the FK itself exactly like a plain field: anull=TrueFK is<type> | Noneand optional, a non-null FK without a default is required. optional=,exclude_readonly=,exclude_sensitive=apply to the id field like to any other field.exclude_readonly=Truekeeps it (the caller supplies it on create). Asensitive=TrueFK drops its id underexclude_sensitive=True.exclude=/include=/optional=accept either the relation name ("author") or the id field name ("author_id") — both select the same field.- An O2O with
primary_key=Truebecomes just its id field, which is the pk;exclude=("owner",)never removes that pk — onlyexclude=("owner_id",)does. - Reverse relations (reverse FK/O2O) have no column on this row, so a flat schema leaves them out
entirely —
PydanticMeta.backward_relationsand annotations are ignored. - M2M relations stay nested submodels (
list[TagSchema]), and still get prefetched. Nested submodels (an M2M target's schema) are built withrelations_as_ids=Truetoo. from_hare_orm()/from_queryset()read the id straight off the instance — no query for the related object. Only M2M relations still in the schema are prefetched.- A schema built with the flag is cached separately from the one built without it; the name of a schema built without it doesn't change.
A GenericForeignKeyField takes the place of its
branches in a schema: on output a union of its targets' schemas told apart by type (the branch name),
on input (exclude_readonly=True) and with relations_as_ids=True {"type": "post", "id": 1} — a
composite key by its key fields. from_hare_orm()/from_queryset() prefetch its branches. The field's
name in exclude=/include= stands for its branches and their key fields: exclude=("target",) leaves
all of them out, include=("id", "target") keeps the field.
Defaults of a model: PydanticMeta¶
Declare a nested class on the hare model itself to set defaults every pydantic_model_creator() call for
that model picks up (an explicit function argument still wins):
class Author(Model):
id = fields.UUIDField(primary_key=True)
name = fields.CharField(max_length=120)
books: fields.ReverseRelation["Book"]
class PydanticMeta:
exclude = ("some_internal_field",)
max_recursion = 2
backward_relations = False # don't pull in `books` unless explicitly annotated
| Option | Default | Meaning |
|---|---|---|
include / exclude |
() / ("Meta",) |
Field allow/deny list. |
computed |
() |
Extra computed fields. |
extra_fields |
{} |
Values read from the row by name, with their types. |
related_values |
{} |
Fields of related rows, flattened. |
backward_relations |
True |
Whether reverse FK/O2O relations are included even without an explicit type annotation requesting them. Not recommended: it can pull in a large amount of data unchecked. |
max_recursion |
3 |
How many levels deep relation traversal goes before stopping. |
allow_cycles |
False |
Allow self-referential recursion. |
exclude_raw_fields |
True |
Drop the FK shadow column (e.g. author_id) once the related object itself is included. |
sort_alphabetically |
False |
Field ordering. |
model_config |
None |
Extra Pydantic ConfigDict. |
Computed fields¶
class Author(Model):
first_name = fields.CharField(max_length=60)
last_name = fields.CharField(max_length=60)
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}"
AuthorSchema = pydantic_model_creator(Author, computed=("full_name",))
A plain method, a @property or a functools.cached_property works. The function's return type
annotation becomes the field's type in the schema. A computed field that touches an unfetched relation
raises NoValuesFetched with a message telling you to either annotate the relation so it gets
auto-prefetched, or load it yourself first with prefetch_related_objects().
Extra fields and related values¶
BookSchema = pydantic_model_creator(
Book,
extra_fields={"reviews_count": int, "author.books_count": int},
related_values={"author_name": "author.name"},
)
book = await Book.objects.select_related("author").annotate(reviews_count=Count("reviews")).get(id=1)
BookSchema.model_validate(book)
extra_fields are values read from the row by name — a query's annotation, an attribute set on the row
after it was read (a request query's api_getter sets them too) — with their types: a query builds them, so
the model can't tell them. related_values put a field of a related row next to the row's own, through
relations to one row; validated from a mapping, the value is taken by its own name too. Both are None where
the row has no value — a query that didn't annotate it, a relation not loaded. A name written as a path
through the relations ("author.books_count") adds the field to the related schema. A name the schema has
already, one declared twice, a path through what is no relation and a related value through a relation to
many rows or ending at a relation raise ConfigurationError. PydanticMeta declares both as well. A write
schema (exclude_readonly=True) gets neither — a row is never written with them; sort_alphabetically
sorts them with the other fields. from_hare_orm()/from_queryset() load the relations a related value
reads through, even when the schema excludes the relation or shows it as an id (relations_as_ids=True).
include and exclude select these fields by their declared schema names, just like model and computed
fields. Paths select extra fields on nested schemas too ("author.books_count"). An excluded related
value adds no relation-loading requirement. The same selection applies when declared in PydanticMeta.
Building instances: PydanticModel / PydanticListModel¶
Every generated schema inherits from PydanticModel (or PydanticListModel for the queryset variant),
which adds ORM-aware constructors on top of the usual model_validate():
class PydanticModel(BaseModel):
# one object - loads its relations itself
@classmethod
async def from_hare_orm(cls, obj: Model) -> Self: ...
@classmethod
async def from_queryset_single(cls, queryset: QuerySetSingle[Model]) -> Self: ...
@classmethod
async def from_queryset(cls, queryset: QuerySet[Model]) -> list[Self]: ...
author = await Author.objects.get(pk=author_id)
schema = await AuthorSchema.from_hare_orm(author)
schemas = await AuthorSchema.from_queryset(Author.objects.filter(name__icontains="le guin"))
list_schema = await AuthorListSchema.from_queryset(Author.objects.all())
All three prefetch whatever relations the schema itself references, so you don't have to call
select_related()/prefetch_related() yourself just to satisfy the schema's own annotations. Plain
AuthorSchema.model_validate(author) also works (that's what from_attributes=True gives you for free),
but it raises NoValuesFetched naming the field if the schema references an FK/O2O/M2M/
backward-relation field you haven't already loaded on author — even a nullable FK whose value is
genuinely None still needs an explicit fetch, since there's no other way to tell "confirmed empty"
apart from "never checked". Use one of the three methods above instead of prefetching by hand.
A schema naming a field the instance doesn't hold raises NoValuesFetched too, in from_hare_orm()
and the other ways alike: a field an .only()/.defer() left out, or a value the database computes
(GeneratedField) a write didn't read back — of a new instance on a database returning no rows of a
write (ClickHouse), or of a clone() not saved yet. Load the field (refresh_from_db()), or leave it out
of the schema.
from_queryset_single() takes a query of one row — .get(...) or .first(): one that reads no row
(first() of none) raises DoesNotExist, as get() does, and a queryset of many rows raises
QueryError (that's from_queryset()'s).
A model's fields by type: ModelDescription¶
from hare.contrib.pydantic.descriptions import ModelDescription
description = ModelDescription.from_model(Book)
What the creator builds a schema from: a model's fields grouped by type, each group a list of Field
objects in declaration order — pk_fields (one per component of a composite key), data_fields (every
other field holding a plain value, the key columns of forward relations such as author_id included),
foreign_key_fields, backward_foreign_key_fields, one_to_one_fields, backward_one_to_one_fields
and many_to_many_fields. The model's relations must be initialized (Hare.init() or
Hare.bind_models()).
pydantic_queryset_creator()¶
def pydantic_queryset_creator(
model: type[Model],
*,
name: str | None = None,
exclude: tuple[str, ...] | None = None,
include: tuple[str, ...] | None = None,
computed: tuple[str, ...] | None = None,
extra_fields: dict[str, Any] | None = None,
related_values: dict[str, str] | None = None,
optional: tuple[str, ...] | None = None,
allow_cycles: bool | None = None,
sort_alphabetically: bool | None = None,
exclude_readonly: bool = False,
meta_override: type | None = None,
model_config: ConfigDict | None = None,
validators: dict[str, Any] | None = None,
module: str = "hare.contrib.pydantic",
exclude_sensitive: bool = False,
relations_as_ids: bool = False,
) -> type[PydanticListModel]: ...
Wraps the single-item schema in a RootModel-based list schema (named <Model>_list by default) —
useful as the return-type annotation for a "list all X" endpoint. name= names the list schema only; the
item schema keeps its default name. Every other argument builds the item schema, as in
pydantic_model_creator().