ModelSchema¶
ModelSchema is a Schema subclass that generates its fields directly from a
Django model, so you don't have to duplicate every field name and type by hand.
Basic usage¶
Set model and fields on an inner Meta class:
from django.contrib.auth.models import User
from ninja import ModelSchema
class UserSchema(ModelSchema):
class Meta:
model = User
fields = ["id", "username", "first_name", "last_name"]
# Equivalent to:
#
# class UserSchema(Schema):
# id: Optional[int] = None
# username: str
# first_name: Optional[str] = None
# last_name: Optional[str] = None
For each field, ModelSchema looks at the Django field type to pick a Python type, and copies
over null/blank (→ optional with a None default), default, help_text (→ description)
and verbose_name (→ title).
Note
model also accepts a string in "app_label.ModelName" form, e.g. model = "auth.User" -
handy when you want to avoid importing the model directly.
Selecting fields¶
fields and exclude are mutually exclusive - use whichever is shorter for your case.
All fields¶
Pass "__all__" to include every field on the model:
Warning
Using "__all__" is not recommended - it can accidentally expose fields you didn't mean to
(like a hashed password). Prefer an explicit fields list.
Excluding fields¶
To take every field except a few, use exclude instead:
class UserSchema(ModelSchema):
class Meta:
model = User
exclude = ["password", "last_login", "user_permissions"]
# Will create a schema with all the remaining fields:
#
# class UserSchema(Schema):
# id: Optional[int] = None
# username: str
# first_name: Optional[str] = None
# last_name: Optional[str] = None
# email: Optional[str] = None
# is_superuser: bool = False
# ... and the rest
One of fields or exclude is required - ModelSchema refuses to build a schema without either
(to avoid silently exposing every field whenever a model gains a new one).
Relations¶
Foreign keys and many-to-many fields are supported out of the box:
- A
ForeignKeybecomes the type of the related model's primary key (intby default). - A
ManyToManyFieldbecomes alistof that primary-key type.
from django.contrib.auth.models import User
from django.db import models
from ninja import ModelSchema
class Tag(models.Model):
name = models.CharField(max_length=50)
class Comment(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE)
tags = models.ManyToManyField(Tag)
text = models.TextField()
class CommentSchema(ModelSchema):
class Meta:
model = Comment
fields = ["id", "author", "tags", "text"]
# author: int (the related User's pk)
# tags: list[int]
When you return a model instance as the response, Ninja resolves author and each item in
tags to their primary key automatically. If you need the related object's own fields nested
in the response instead of just its id, declare that field explicitly with another schema (see
Overriding and adding fields), or build one with
create_schema(..., depth=...).
Foreign key ids (author_id)¶
Meta.fields takes Django field names, so a ForeignKey is listed as author, not by its
database column author_id - the latter raises a ConfigError. If you want the raw id under
the author_id name (for example next to a nested author object), declare it as a regular
annotated field. It reads obj.author_id straight from the instance, with no extra query:
class UserSchema(ModelSchema):
class Meta:
model = User
fields = ["id", "username"]
class CommentSchema(ModelSchema):
author: UserSchema # nested object
author_id: int # plain id
class Meta:
model = Comment
fields = ["id", "text"]
# {"id": 1, "text": "...", "author": {"id": 3, "username": "jane"}, "author_id": 3}
Making fields optional¶
For PATCH-style endpoints you often want every field optional regardless of what's required on
the model. Use fields_optional:
from django.contrib.auth.models import Group
class PatchGroupSchema(ModelSchema):
class Meta:
model = Group
fields = ["id", "name"] # name is required on the model
fields_optional = "__all__"
Or optionally make only a subset optional:
When applying a patch payload to an instance, use exclude_unset=True so that fields the client
didn't send aren't reset to None:
@api.patch("/patch/{pk}", response=PatchGroupSchema)
def patch(request, pk: int, payload: PatchGroupSchema):
obj = get_object_or_404(Group, pk=pk)
# Notice `exclude_unset=True` - only fields present in the request are applied
for attr, value in payload.dict(exclude_unset=True).items():
setattr(obj, attr, value)
obj.save()
return obj
PatchDict¶
PatchDict is a shortcut for the same pattern: wrap any Schema in PatchDict[...] to get a
version where every field is optional, and the validated payload comes back as a plain dict
containing only the fields that were actually sent - no manual fields_optional or
exclude_unset needed.
from ninja import NinjaAPI, PatchDict, Schema
class GroupSchema(Schema):
# fields don't need to be declared Optional - PatchDict takes care of that
name: str
api = NinjaAPI()
@api.patch("/groups/{pk}", response=GroupSchema)
def update_group(request, pk: int, payload: PatchDict[GroupSchema]):
obj = get_object_or_404(Group, pk=pk)
for attr, value in payload.items():
setattr(obj, attr, value)
obj.save()
return obj
Here payload is a dict validated against GroupSchema, containing only the keys present in
the request body.
Overriding and adding fields¶
To change the type of a generated field, or add a field that doesn't exist on the model, declare
it as a normal annotated attribute - it takes priority over whatever ModelSchema would have
generated:
class GroupSchema(ModelSchema):
class Meta:
model = Group
fields = ["id", "name"]
class UserSchema(ModelSchema):
groups: list[GroupSchema] = []
class Meta:
model = User
fields = ["id", "username", "first_name", "last_name"]
This is also how you nest a related object's full schema instead of just its primary key (see Relations above), and how you attach validators, defaults or a different type to any individual field.
Don't list a declared field in Meta.fields as well. The declaration already includes it and
takes priority, so the generated field would be ignored anyway. ModelSchema emits a
DeprecationWarning when a name appears in both places; this will become an error in a future
release.
Methods work as usual too - a ModelSchema is still a regular Schema/Pydantic model:
class UserSchema(ModelSchema):
class Meta:
model = User
fields = ["id", "username"]
def greet(self) -> str:
return f"Hello, {self.username}!"
Custom field types¶
For each Django field, ModelSchema maps the field's internal type (Field.get_internal_type())
to a Python type. This covers all the built-in Django field types, but if you use a field type it
doesn't know about (a custom field, or one from a third-party package), you'll get a ConfigError
telling you to register it:
# models.py
from django.db import models
import pgvector
class MyModel(models.Model):
embedding = pgvector.VectorField()
# schemas.py
from ninja.orm import register_field
register_field("VectorField", list[float])
register_field takes the Django field's internal type name (a string) and the Python type to
use for it, and only needs to run once (e.g. at import time in your schemas.py) before any
ModelSchema that uses that field type is defined.
Registering a field class¶
A custom field that subclasses a built-in one inherits its internal type - e.g. a subclass of
UUIDField reports "UUIDField" - so registering by name would change every UUIDField.
Pass the field class instead to target just that field (and its subclasses):
from typing import Annotated
from pydantic import BeforeValidator
from ninja.orm import register_field
class PublicIDField(models.UUIDField):
"stored as uuid, but returns a custom PublicID object"
def from_db_value(self, value, expression, connection):
return PublicID(value)
register_field(PublicIDField, Annotated[str, BeforeValidator(str)])
A class registration takes priority over a registration by name, and the most specific class
wins. It also applies to foreign keys and one-to-one fields that point at a model whose primary
key (or to_field) is that field.
Edge cases & tips¶
fieldsandexcludecan't be combined - pick one per schema; mixing them raises aConfigError.- Reverse relations are skipped. Reverse foreign keys and reverse many-to-many accessors
(the ones Django adds automatically, not a field you declared) are never included, even with
fields = "__all__"; include them explicitly with your own annotated field if you need them. TextField.max_lengthis a form-level hint in Django, not a database constraint, so Ninja enforces it only on incoming request data, not when serializing an existing (possibly longer) value in a response.- Schema caching. Identical calls with the same model, name, fields, excludes and optional
fields reuse the same generated class rather than rebuilding it, so defining the same
ModelSchematwice (e.g. in tests) is cheap. - Prefer
ModelSchemawhen a schema should track a model's fields declaratively; reach forcreate_schema()instead when you need to build a schema dynamically at runtime (e.g. the model or field list is only known at call time).