In Django Ninja, when would you declare a plain Schema instead of a ModelSchema, and what must a ModelSchema's Meta class contain?
answer
- one is hand-written, one is derived
- Meta model plus fields or exclude
- null, blank, pk become optional
- input contract differs from the table
basics
~20 sA Schema is a hand-written Pydantic model; a ModelSchema derives its fields from a Django model through Meta, which needs model plus exactly one of fields or exclude. Use Schema when the API contract differs from the table, typically for input.
solid answer
~40 s`ninja.Schema` is a Pydantic `BaseModel` with `from_attributes=True`, dotted-path aliases and `resolve_<field>` resolvers, so it reads straight from model instances. `ModelSchema` generates its fields from a Django model: its inner `Meta` needs `model` (a class or an `"app_label.ModelName"` string) and exactly one of `fields` or `exclude`; omitting both, setting both, or naming a field the model lacks raises `ConfigError` when the class is defined. `fields_optional` makes some or all fields optional for PATCH. The mapping is shallow: `null=True`, `blank=True` and the primary key become `Optional` with a `None` default, a `ForeignKey` becomes the related primary-key value, and the field's `choices` and `validators` are not carried over. So I use `ModelSchema` for output that mirrors the table and a plain `Schema` for input, where the contract and the rules differ.
code
python · 28 linesfrom datetime import date
from django.db import models
from ninja import ModelSchema, Schema
class Booking(models.Model):
room = models.ForeignKey("hotels.Room", on_delete=models.PROTECT)
guest_name = models.CharField(max_length=120)
check_in = models.DateField()
check_out = models.DateField()
guests = models.PositiveSmallIntegerField(default=1)
notes = models.TextField(blank=True)
class BookingIn(Schema):
room_id: int
guest_name: str
check_in: date
check_out: date
guests: int = 1
notes: str = ""
class BookingOut(ModelSchema):
class Meta:
model = Booking
fields = ["id", "room", "guest_name", "check_in", "check_out", "guests"]go deeper
Recall that Schema is hand-written and ModelSchema is generated from a model through Meta with model and fields or exclude.
Explain the field mapping: which options become Optional, how relations appear, and which ConfigErrors fire at import time.
Show why input and output deserve different schemas: dropped choices and validators, blank fields becoming None, and fields='all' leaking columns.
Set a team convention for schema ownership, such as explicit input schemas and ModelSchema only for reviewed output lists, so the API contract does not drift with migrations.
## Two ways to declare a schema In **Django Ninja**, a **schema** is the class that describes a request body or a response. There are two base classes: - **`ninja.Schema`** - a Pydantic `BaseModel` subclass whose `model_config` sets `from_attributes=True`. Every field is written by hand. On top of Pydantic it adds dotted aliases such as `Field(None, alias="room.name")`, `resolve_<field>` resolvers, and a getter that turns a related `Manager` into a list, calls callables such as `get_status_display`, and turns a `FieldFile` into its URL. - **`ninja.ModelSchema`** - a `Schema` whose fields are **generated from a Django model** when the class is created, configured through an inner `Meta` class. Both validate input and serialise output the same way; the difference is only where the field list comes from. ## What ModelSchema's Meta accepts | attribute | meaning | |---|---| | `model` | the Django model class, or a lazy `"app_label.ModelName"` string | | `fields` | a list of field names, or `"__all__"` | | `exclude` | a list of field names to leave out | | `fields_optional` | a list of names, or `"__all__"`, to make optional (for PATCH bodies) | The rules are enforced with `ninja.errors.ConfigError` **at import time**, when the class statement runs: 1. Neither `fields` nor `exclude` set - error; Ninja refuses to expose every column by accident. 2. Both `fields` and `exclude` set - error. 3. A name that is not a field of the model - error. 4. A nested `Config` class instead of `Meta` - error; that older configuration style was removed. 5. A Django field type Ninja cannot map - error suggesting `ninja.orm.register_field()`. `fields = "__all__"` is allowed but the docs warn against it: on a user model it would publish the password hash. Reverse relations (the `booking_set` side) are never generated. ## How Django fields become schema fields The generated field is derived from the model field's internal type and a few options: - the Python type comes from a lookup table (`CharField` to `str`, `DateField` to `date`, `PositiveSmallIntegerField` to plain `int`); - `max_length` on a `CharField` becomes a length constraint; - `null=True`, `blank=True` or being the **primary key** makes the field `Optional` with a default of `None`; - a model `default` becomes the schema default, a callable default becomes a default factory; - `help_text` becomes the description and `verbose_name` the title; - a `ForeignKey` becomes the **related row's primary-key value**, a `ManyToManyField` a list of them - not nested objects. What is **not** carried over matters as much: the field's `choices`, its `validators`, and anything in the model's `clean()`. A `PositiveSmallIntegerField` accepts `-3` in the schema; only the database constraint catches it later. You can add or override any field by annotating it on the class, for example `room: RoomOut` to nest the related object. ## Overriding and nesting fields A `ModelSchema` is still an ordinary class, so annotations written on it are merged with the generated ones and win over them: - `room: RoomOut` replaces the primary-key integer with a nested object built by another schema, read from `booking.room` when serialising; - `nights: int` adds a field the model does not have, to be filled by a resolver; - `guest_name: str = Field(..., max_length=60)` tightens a generated rule for this API only. Nesting has a cost that belongs to the view, not the schema: serialising `room` for a list of bookings reads the relation on every row, so the queryset should load it up front. For a custom model field class, `ninja.orm.register_field("VectorField", list[float])` tells the generator which Python type to use; without it the class fails with `ConfigError` at import. ## Choosing for a booking endpoint For a booking API the output usually mirrors the table closely, so `ModelSchema` saves repetition and stays in sync as columns are added to the listed `fields`. The input is different: - the client sends `room_id`, `check_in`, `check_out` and `guests`, but never `status` or `created_at`; - `check_out` must come after `check_in`, a cross-field rule the model fields do not express; - `notes = models.TextField(blank=True)` would become `Optional[str]` defaulting to `None`, and passing that `None` to `Booking.objects.create()` fails on a `NOT NULL` column. A hand-written `BookingIn(Schema)` states that contract explicitly; a `BookingOut(ModelSchema)` lists what is safe to show. ## Summary of the trade-off | | `Schema` | `ModelSchema` | |---|---|---| | field list | written by hand | generated from `Meta` | | drift from the model | possible, silent | follows listed fields | | input rules | whatever you declare | only type, length, null/blank, default | | typical use | request bodies, computed or aggregated output | responses that mirror one model |
- How do you build a PATCH schema for bookings with Django Ninja?Either a `ModelSchema` with `fields_optional = "__all__"` (or a list) in `Meta`, then `payload.dict(exclude_unset=True)` so absent fields are not set to `None`; or annotate the parameter as `PatchDict[BookingIn]`, which yields a dict containing only the fields the client actually sent.
- Why is using a ModelSchema as the create-booking input risky even with an explicit fields list?The generated rules are shallow: `blank=True` fields become `Optional` defaulting to `None`, `choices` and field `validators` are dropped, and a positive integer field is a plain `int`. Bad values pass the schema and fail later at `save()` as a database error, a 500 instead of a 422.
saying these in an interview costs you the question
- A ModelSchema without fields or exclude simply includes every model field.
- ModelSchema enforces the model field's choices and validators on input.
- A ForeignKey in a ModelSchema is serialised as a nested object by default.
- blank=True only affects Django forms, so the schema field stays required.
- ModelSchema is configured with an inner Config class.