Schemas¶
Schema is Django Ninja's own base class for describing the shape of data —
the fields a request body must contain, or the fields an operation's response
will expose. It's a pydantic.BaseModel subclass with a few Django-friendly
additions, and it's called Schema (rather than Model) purely to avoid
clashing with django.db.models.Model.
This page covers how to define a Schema class itself. To use one as a
request body see Request Body; to use one as an operation's
response= see Responses.
Defining a schema¶
A schema is a class that inherits from ninja.Schema with its fields
declared as type-annotated attributes, exactly like a Pydantic model:
A field without a default, such as username and password above, is
required. A field with a default, such as is_active, is optional —
if it's missing from the input, the default is used instead. Use
field: type | None = None for a field that's optional and may explicitly be
null.
Any type Pydantic understands works here: str, int, float, bool,
datetime.date / datetime.datetime, UUID, Decimal, Enum, list[...],
dict[...], other Schema classes, and so on — see
Pydantic's field types
for the full list.
It's common to use different schemas for what an operation accepts and
what it returns — for example, to keep a password field out of the
response:
from django.contrib.auth.models import User
from ninja import NinjaAPI, Schema
api = NinjaAPI()
class UserIn(Schema):
username: str
password: str
class UserOut(Schema):
id: int
username: str
@api.post("/users", response=UserOut)
def create_user(request, data: UserIn):
user = User.objects.create_user(data.username, password=data.password)
return user
Django Ninja uses UserOut to validate and serialize whatever create_user
returns, and to generate its OpenAPI schema — which also means the response
is limited to the fields declared on UserOut, even if the returned
object (a Django User instance here) has other attributes.
Nested schemas and lists¶
A schema field can be another Schema, to represent a nested object, or a
list[...] of one, to represent a collection:
from ninja import NinjaAPI, Schema
api = NinjaAPI()
class OwnerSchema(Schema):
id: int
first_name: str
class TaskSchema(Schema):
id: int
title: str
is_completed: bool
owner: OwnerSchema | None = None
@api.get("/tasks", response=list[TaskSchema])
def tasks(request):
return [
{"id": 1, "title": "Write docs", "is_completed": False, "owner": {"id": 1, "first_name": "Sam"}},
{"id": 2, "title": "Review PRs", "is_completed": False, "owner": None},
]
Nesting works from ORM objects too, not just dicts. Given a Task model with
a nullable owner foreign key, TaskSchema above validates straight off a
queryset — no dicts required:
from django.contrib.auth.models import User
from django.db import models
class Task(models.Model):
title = models.CharField(max_length=200)
is_completed = models.BooleanField(default=False)
owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
@api.get("/tasks", response=list[TaskSchema])
def tasks(request):
return Task.objects.select_related("owner")
[
{"id": 1, "title": "Write docs", "is_completed": false, "owner": {"id": 1, "first_name": "Sam"}},
{"id": 2, "title": "Review PRs", "is_completed": false, "owner": null}
]
A task without an owner serializes owner as null, matching the
OwnerSchema | None = None field above.
Watch out for N+1 queries
Serializing owner touches task.owner for every task in the list —
without select_related("owner") on the queryset, that's one extra
query per task. Reach for select_related on a ForeignKey /
OneToOneField like this one, and prefetch_related on a to-many or
reverse relation, to fetch everything up front instead.
This also works through reverse relations. When the source value for a field
is a Django Manager or QuerySet (a related_set, for instance), Django
Ninja automatically evaluates it into a plain list before validation, so you
can type the field as list[ChildSchema] directly, without calling .all()
or wrapping it in list() yourself.
Aliases¶
By default, a schema field is read from the identically-named attribute (or
dict key) on the object you're serializing. Give a field an
alias to read it from somewhere else instead — Schema extends Pydantic's
Field(..., alias=...) to also follow dotted paths and to call
attributes that turn out to be callable:
from ninja import Field, Schema
class Owner:
def __init__(self, first_name):
self.first_name = first_name
def get_title(self):
return "owner"
class Task:
def __init__(self, id, is_completed, owner):
self.id = id
self.is_completed = is_completed
self.owner = owner
class TaskSchema(Schema):
id: int
# the first Field() argument is the default — use ... for a required field
completed: bool = Field(..., alias="is_completed")
owner_first_name: str = Field(None, alias="owner.first_name")
owner_title: str = Field(None, alias="owner.get_title") # called automatically
TaskSchema.from_orm(Task(id=1, is_completed=True, owner=Owner("Sam"))).dict()
# {"id": 1, "completed": True, "owner_first_name": "Sam", "owner_title": "owner"}
Dotted aliases are resolved with Django's template variable lookup, so they
also work against dict keys and list/tuple indexes along the path, for
example alias="message_set.0.text".
The call-following behavior above is also the common way to expose a Django
choices field's display value — Django generates a get_<field>_display()
method for any field with choices=, so aliasing straight to it works
without a resolver:
Aliases and plain dict input
Dotted-path resolution only kicks in when the source object is not a
dict — a dict source is only ever looked up by its exact top-level
key. When you validate from a dict (as most request bodies do), use a
plain, non-dotted alias if you need one.
Resolvers¶
A resolver computes a field's value with a method named resolve_<field>
instead of reading it from an attribute or alias. It must be a @staticmethod
that takes the object being serialized as its only required argument:
from ninja import Schema
class TaskSchema(Schema):
id: int
title: str
lower_title: str
@staticmethod
def resolve_lower_title(obj):
return obj.title.lower()
Resolvers must be @staticmethod
Only @staticmethod resolvers are currently supported. A resolver
defined as a regular method (expecting self) raises
NotImplementedError when it runs — always use @staticmethod.
A resolver overrides whatever value the object would otherwise supply for
that field — including one that also has a matching attribute — so it's also
a way to add a field that doesn't exist on the source object at all, as
lower_title does above.
Accessing context¶
Pydantic v2's serialization/validation context is available to a resolver
too — add a context argument (or **kwargs) to receive it:
from ninja import Schema
class TaskSchema(Schema):
id: int
path: str = ""
@staticmethod
def resolve_path(obj, context):
request = context["request"]
return request.path
When a schema like this is used as a response= for an operation, Django
Ninja automatically passes {"request": request, "response_status": <code>}
as the context — no setup required. The same applies the other way round: a
schema used as request input (a body parameter, for instance) is
validated with {"request": request} as its context, so a resolver or
validator on an input schema can read context["request"] too. Outside a
view, pass your own:
Files and images¶
Django's FileField and ImageField are, by default, converted to their
str URL rather than serialized as objects. Given a model with
image = models.ImageField(upload_to="images"), just declare the matching
schema field as str (or str | None) and Django Ninja does the rest:
Serializing an object whose image attribute is a FieldFile produces its
.url as a plain string, e.g. "/media/images/zebra.jpg". If the field can
be empty, declare it str | None — an empty FieldFile then serializes to
None instead of failing validation; with a plain str field, an empty
value still raises a validation error.
Self-referencing schemas¶
To create a schema that references itself — a tree of comments with
replies, for instance — put the type in quotes and call model_rebuild()
once the class is fully defined:
from ninja import Schema
class Comment(Schema):
text: str
replies: "list[Comment] | None" = None
Comment.model_rebuild() # required to resolve the quoted type above
Self-referencing a schema from create_schema()
A schema generated with create_schema() needs the
same treatment, but its class name must be passed explicitly via
create_schema(..., name="...") so it's addressable for
model_rebuild() — see create_schema.
Schema vs. plain Pydantic models¶
Schema is a pydantic.BaseModel — every Pydantic feature (validators,
computed fields, model_config, serialization, JSON Schema...) is available
unchanged, and everything above works on top of it. The differences that
matter day to day:
from_attributes=Trueby default. A plain Pydantic model has to opt into reading from objects withmodel_config = ConfigDict(from_attributes=True); aSchemaalways can, in addition to reading from adict.- Alias resolution is enhanced, as covered above: dotted paths, list/dict
indexing, and automatically calling callables (
Field(alias="get_x")). ManagerandQuerySetattributes are auto-evaluated into plain lists during serialization, so a to-many relation typed aslist[ChildSchema]doesn't need an explicit.all().FileField/ImageFieldvalues become their.url(orNone, when the field is typed to allow it), as covered above.- Resolvers must be
@staticmethod— see above. - A couple of Pydantic v1-style convenience methods are kept around:
.from_orm(obj)is an alias for.model_validate(obj), and.dict()is an alias for.model_dump().
See Schema Configuration for the model_config options
that matter most in Django Ninja (alias generators, extra, validation
modes), and ModelSchema / create_schema
for generating a Schema straight from a Django model instead of writing
one by hand.
Serializing outside a view¶
Everything above also works directly, without going through an operation —
useful in management commands, signal handlers, or tests. .from_orm()
validates an object (or a queryset/list of them) into schema instances, and
.dict() / .model_dump_json() give you plain data back out:
>>> class PersonSchema(Schema):
... name: str
...
>>> person = Person.objects.get(id=1)
>>> data = PersonSchema.from_orm(person)
>>> data
PersonSchema(name='Mr. Smith')
>>> data.dict()
{'name': 'Mr. Smith'}
>>> [PersonSchema.from_orm(p).dict() for p in Person.objects.all()]
[{'name': 'Mr. Smith'}, {'name': 'Mrs. Smith'}]
.schema() is deprecated
If you're carrying over habits from Pydantic v1, use .model_json_schema()
(or Ninja's own .json_schema(), which additionally cleans up the JSON
Schema output for OpenAPI) — the old .schema() still works but emits a
DeprecationWarning.