pypi django-ninja 1.8.0b1

3 hours ago

What's New

Webhooks

You can now document the webhooks your API sends. Decorate the payload Schema with @api.webhook or @router.webhook and it appears in the OpenAPI 3.1 webhooks section, so Swagger and Redoc show it next to your operations. This is documentation only: Ninja doesn't send anything. The guide shows one way to deliver them with Django tasks.

from ninja import NinjaAPI, Router, Schema

api = NinjaAPI()

@api.webhook("order.paid")
class OrderPaid(Schema):
    id: int
    total: Decimal

router = Router(tags=["orders"])

@router.webhook  # name defaults to the class name: "OrderShipped"
class OrderShipped(Schema):
    id: int

New documentation (and docs for AI assistants)

The docs at django-ninja.dev are reorganized into four sections:

  • Quick Start: build an events API in three pages
  • Tutorial: build TaskFlow, a team task tracker, in eight parts. It covers relations, validation, JWT auth and permissions, filtering, uploads, testing, OpenAPI polish and versioning
  • Guide: one short page per topic, and every example was run against the current code
  • About

The docs now include files for LLMs. Point your coding assistant at /llms.txt, which has usage notes and a link to every page, or at /llms-full.txt, which is the whole Guide in one file. Every page is also served as raw Markdown, e.g. /guide/routers.md.

register_field() accepts field classes

A custom Django field that subclasses a built-in one inherits its internal type, so registering it by name ("UUIDField") would change every UUIDField. You can now register the field class itself:

from typing import Annotated
from pydantic import BeforeValidator
from ninja.orm import register_field

class PublicIDField(models.UUIDField):
    def from_db_value(self, value, expression, connection):
        return PublicID(value)

register_field(PublicIDField, Annotated[str, BeforeValidator(str)])

The registration also applies to subclasses, and the most specific class wins. It also covers ForeignKey and OneToOne fields that point at that field. Forward FK and OneToOne fields now take their type from the to_field target instead of always using the related model's pk.

Optional schema parameters

An Optional schema used as a Query, Header or Form param now falls back to its default when none of its fields are sent:

class Filters(Schema):
    city: str
    year: int = 2026

@api.get("/events")
def events(request, filters: Filters | None = Query(None)):
    ...

# GET /events            -> filters is None   (was 422)
# GET /events?city=Kyiv  -> Filters(city="Kyiv", year=2026)

Body params next to files

When you mix Body params with File or Form, each multipart part is now read the way Swagger and generated clients send it. Primitive types (str, int, date, Enum, EmailStr, ...) are read as plain text, and objects and arrays are read as JSON:

@api.post("/upload")
def upload(
    request,
    file: File[UploadedFile],
    note: str = Body(...),
    day: date = Body(...),
    meta: dict[str, int] | None = Body(None),
):
    ...

# note='say "hi"'  day=2024-01-02  meta='{"a": 1}'  -> 200 (quoted strings and dates used to give 400)

ModelSchema improvements

Model fields now always come first, in model order, followed by your custom fields. A field you declare on the schema is used as-is, so a third-party model field that Ninja can't convert no longer breaks fields = "__all__":

class ProductOut(ModelSchema):
    price: str            # MoneyField: no register_field() needed when declared
    tags: list[str] = []

    class Meta:
        model = Product
        fields = "__all__"

list(ProductOut.model_fields)  # ['id', 'name', 'price', 'tags']

Listing a FK by its attname (user_id) in Meta.fields now gives an error that suggests the fix: use user, or declare user_id: int.

Python 3.15

Django Ninja is now tested on Python 3.15 with Django 5.2, 6.0 and 6.1.

⚠️ Behavior changes

  • The NINJA_PAGINATION_MAX_OFFSET setting is removed, and a value left in settings is ignored (#1818)
  • ForeignKey(..., to_field="slug") is now typed from the target field (string) instead of the related pk (integer). If a string to_field holds numeric values, responses now return "123" instead of 123 (#1842)
  • ModelSchema puts model fields (in Meta.fields order) before custom fields. This changes the key order in responses and in OpenAPI (#1829)
  • Declaring a field on a ModelSchema and also listing it in Meta.fields now emits a DeprecationWarning. It will become a ConfigError in a later release (#1815)
  • Nullable FK and OneToOne fields with depth > 0 are now Optional in the schema (anyOf: [..., null]) (#1803)
  • Paginated responses of Union/Annotated items get their own schema names (PagedFooOrBar instead of a shared PagedUnion or PagedAnnotated). Models in generated clients may be renamed (#1800)
  • In multipart requests, a str Body part sent with literal surrounding quotes now keeps them (#1825)

Details

Features

  • Webhooks: @api.webhook / @router.webhook for OpenAPI 3.1 webhooks by @vitalik in #1783
  • ModelSchema: register_field() accepts field classes; FK/O2O typed from to_field target by @vitalik in #1842
  • Add Python 3.15 to CI matrix and classifiers by @vitalik in #1837

ModelSchema & ORM

  • ModelSchema: don't convert model fields that are declared on the schema by @vitalik in #1843
  • Fix ModelSchema field order: model fields before custom fields by @vitalik in #1829
  • Warn when a ModelSchema field is declared as a class attribute and also listed in Meta.fields by @smoltafet in #1815
  • ModelSchema: hint about ForeignKey attnames (user_id) in Meta.fields by @vitalik in #1832
  • Fix nullable relations not Optional in schemas with depth > 0 by @moneebullah25 in #1803
  • Include base_class in the ModelSchema factory cache key by @moneebullah25 in #1804
  • Fix create_schema crash on models with a ForeignObject reverse relation by @shivanij1203 in #1746

Request parsing & params

  • Read multipart Body parts by their schema: primitives as plain text, objects and arrays as JSON, by @vitalik in #1825 and #1850
  • Fix Optional schema params returning 422 when no fields are sent by @vitalik in #1826
  • Fix numeric aliases (e.g. alias="0") nulling out fields on object input by @smoltafet in #1817

Responses & schemas

  • Filter the response by the declared schema when a subclass instance is returned (also in 1.7.3) by @vitalik in #1824
  • Fix exclude_unset for nested Schema instances, while nested instances are still re-validated by @vitalik in #1830 and #1846

Pagination

  • Fix CursorPagination returning 500 for malformed cursors by @moneebullah25 in #1794
  • Fix CursorPagination 500 on tie runs of 100+ rows by @vitalik in #1818
  • Name paginated union schemas after their members by @ybai08 in #1800

OpenAPI

  • Fix OpenAPI parameter examples for list and dict forms (fixes the Swagger UI i.get is not a function crash) by @vitalik in #1844

Routers & auth

  • Fix auth/throttle overrides for nested routers (also in 1.7.2) by @vitalik in #1821

Typing

  • Parameterize bare Callable and Dict annotations on operations and the test client by @NorthIsUp in #1789

Documentation

  • Restructure into Quick Start / Tutorial / Guide / About by @vitalik in #1786
  • Build single-file Markdown docs for LLMs (llms.txt, llms-full.txt) by @Nuung in #1799
  • Drop typing.List/Dict/Optional in favor of builtin generics by @vitalik in #1782
  • Drop Status from single-response Quick Start examples by @vitalik in #1790
  • Use constant_time_compare in auth examples by @Highdrien in #1781
  • Using request.auth with VIEW-mode decorators by @vitalik in #1831
  • Clarify 404 handling for undefined URLs by @vitalik in #1839
  • Fix typos in docs, a comment and a test docstring by @selmanfarukyilmaz in #1796

Maintenance

New Contributors

Full Changelog: v1.7.1...v1.8.0b1

Don't miss a new django-ninja release

NewReleases is sending notifications on new releases.