Skip to content
Tutorial Step 1 of 7

Define the model

In this step you create the Article model and tell the framework which fields each operation uses.

Schema kinds

Every operation has its own schema. You only declare the ones you need.

Kind Used for If you leave it out
create The body of POST No create endpoint
update The body of PATCH No update endpoint
read List responses and single objects No list or retrieve endpoints
detail Single object responses read is used instead

Write the model

Choose where the schemas live. Both options give you the same API, see choosing a serializer.

blog/models.py
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)

    class Schemas:
        create = SchemaConfig(
            fields=["title", "body"],
            optionals=[("is_published", bool)],
        )
        update = SchemaConfig(
            optionals=[("title", str), ("body", str), ("is_published", bool)],
        )
        read = SchemaConfig(
            fields=["id", "title", "is_published", "created_at"],
        )
        detail = SchemaConfig(
            fields=["id", "title", "body", "is_published", "views", "created_at"],
        )
blog/models.py
from django.db import models


class Article(models.Model):
    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)
blog/serializers.py
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"],
            optionals=[("is_published", bool)],
        )
        update = SchemaConfig(
            optionals=[("title", str), ("body", str), ("is_published", bool)],
        )
        read = SchemaConfig(
            fields=["id", "title", "is_published", "created_at"],
        )
        detail = SchemaConfig(
            fields=["id", "title", "body", "is_published", "views", "created_at"],
        )

The list shows a short version of each article, and a single article shows the full text.

The rest of the tutorial shows the ModelSerializer version. With a Serializer, put the same Schemas on your serializer class.

SchemaConfig options

Option What it does
fields Required fields, in the order they appear
optionals Fields the client may leave out, as (name, type) pairs
customs Extra values that are not model fields, as (name, type, default)
excludes Every field except these

Validate input

Add Pydantic validators to a CreateValidators or UpdateValidators class:

blog/models.py
from pydantic import field_validator


class Article(ModelSerializer):
    # fields and Schemas as above

    class CreateValidators:
        @field_validator("title")
        @classmethod
        def title_not_blank(cls, value: str) -> str:
            if not value.strip():
                raise ValueError("Title cannot be blank")
            return value.strip()

Invalid input returns 422 with the error message.

Create the table

Bash
python manage.py makemigrations blog
python manage.py migrate

Django checks your Schemas when it starts. A typo in a field name shows up as an error before the first request:

Bash
python manage.py check

Try it from Python

Open python manage.py shell and use the model directly:

Python
from blog.models import Article

article = Article.create({"title": "Hello", "body": "First post"})
Article.model_dump(article)
# {'id': 1, 'title': 'Hello', 'body': 'First post', 'is_published': False, ...}
Python
from blog.models import Article

article = await Article.acreate({"title": "Hello", "body": "First post"})
await Article.amodel_dump(article)
# {'id': 1, 'title': 'Hello', 'body': 'First post', 'is_published': False, ...}

model_dump() uses the detail schema. Pass schema=Article.read_schema to get the short version.

Next: create the CRUD API