Schemas¶
Schemas decide which fields your API accepts and returns. This page shows how
to declare them, every SchemaConfig option, and how to use the generated
schemas in your own code.
Declare the schemas¶
Add a Schemas class to your model with one SchemaConfig for each kind:
from django.db import models
from ninja_aio import ModelSerializer, SchemaConfig
class Article(ModelSerializer):
title = models.CharField(max_length=200)
body = models.TextField()
is_published = models.BooleanField(default=False)
views = models.PositiveIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
category = models.ForeignKey(
Category, on_delete=models.CASCADE, related_name="articles"
)
class Schemas:
create = SchemaConfig(
fields=["title", "body", "category"],
optionals=[("is_published", bool)],
)
update = SchemaConfig(
optionals=[("title", str), ("body", str), ("is_published", bool)],
)
read = SchemaConfig(fields=["id", "title", "category", "created_at"])
detail = SchemaConfig(
fields=["id", "title", "body", "is_published", "views", "category", "created_at"],
)
Each kind is used by these endpoints:
| Kind | Used for | When you omit it |
|---|---|---|
create |
The body of POST /api/articles/ |
No create endpoint |
update |
The body of PATCH /api/articles/{id}/ |
No update endpoint |
read |
List items, and the create and update responses | No list or retrieve endpoints |
detail |
The response of GET /api/articles/{id} |
The read fields are used |
With a Serializer, the Schemas class is the same. Only the Meta class
is added:
from ninja_aio import Serializer, SchemaConfig
from .models import Article
class ArticleSerializer(Serializer[Article]):
class Meta:
model = Article
class Schemas:
create = SchemaConfig(fields=["title", "body", "category"])
read = SchemaConfig(fields=["id", "title", "category", "created_at"])
SchemaConfig options¶
| Option | What it does |
|---|---|
fields |
Model fields to include. Input fields are required |
optionals |
Fields the client may leave out, as (name, type) pairs. They default to None |
customs |
Extra values that are not model fields, as (name, type) for required values or (name, type, default) |
excludes |
Include every model field except these |
relations_as_id |
Return these relations as their primary key. read and detail only |
nested |
Create related objects in the same request, like {"comments": Comment}. create on ModelSerializer only |
model_config |
A Pydantic ConfigDict for the generated schema |
An optional field that the client leaves out is skipped. On create the model
default is used, and null is treated the same. On update the current value
is kept: an update only touches the fields the client sent. An explicit null
is saved when the type allows it, like optionals=[("note", str | None)] on a
nullable field.
A foreign key in create fields becomes <name>_id in the request body, like
"category_id": 1. In update, declare it as optionals=[("category", int)],
or ("category", int | None) for a nullable foreign key the client may clear.
See Relations and Nested writes.
Accept extra input values¶
Use customs for values the client sends that are not saved on the model:
create = SchemaConfig(
fields=["title", "body", "category"],
customs=[("notify_editors", bool, False)],
)
The request accepts notify_editors. Read its value in the custom_actions
hook, see Hooks.
Return computed values¶
In read and detail, a custom value comes from the attribute or property
with the same name on the object:
class Article(ModelSerializer):
# fields as above
@property
def summary(self) -> str:
return self.body[:100]
class Schemas:
read = SchemaConfig(
fields=["id", "title"],
customs=[("summary", str, "")],
)
When the object has no such attribute, the default is returned.
You can also put the tuple straight into fields:
read = SchemaConfig(fields=["id", "title", ("summary", str, "")])
This works in create and update too, like
fields=["title", ("source", str, "web")]. There the value is not saved on the
model: read it in custom_actions, like any value in customs.
Change the schema settings¶
Pass a Pydantic ConfigDict to model_config:
from pydantic import ConfigDict
create = SchemaConfig(
fields=["title", "body", "category"],
model_config=ConfigDict(str_strip_whitespace=True),
)
Here " Hello " is saved as "Hello".
Use the generated schemas¶
Each kind is available as a class attribute:
Article.create_schema
Article.update_schema
Article.read_schema
Article.detail_schema
Article.get_schema("read") returns the same class as Article.read_schema.
A kind without a SchemaConfig returns None, except detail, which falls
back to the read fields.
Use them like any Pydantic model:
data = Article.create_schema(title="Hello", body="...", category_id=1)
Schema names in OpenAPI¶
The schemas appear in the OpenAPI document with the model name in lowercase:
| Kind | Name |
|---|---|
create |
articleSchemaIn |
update |
articleSchemaPatch |
read |
articleSchemaOut |
detail |
articleDetailSchemaOut |
| List response | PaginatedarticleSchemaOut |
Catch mistakes at startup¶
Django checks your Schemas when the project starts. Run the checks
yourself with:
python manage.py check
| Code | Problem |
|---|---|
ninja_aio.E001 |
An attribute of Schemas is not create, update, read or detail |
ninja_aio.E002 |
A kind is not a SchemaConfig |
ninja_aio.E003 |
A name in fields, optionals, excludes, relations_as_id or nested is not a field of the model |
ninja_aio.E004 |
A field is listed in both fields and optionals |
ninja_aio.E005 |
relations_as_id or nested is set on a kind that does not support it |
ninja_aio.E006 |
A relations_as_id entry is not a relation |