Release Notes¶
v2.23.0 Latest
Feb 23, 2026📋 Release Notes
🏷️ [v2.23.0] - 2026-02-23
🔧 Improvements
🐍 NotFoundError Class-Name Key Uses snake_case
ninja_aio/exceptions.py
When NINJA_AIO_NOT_FOUND_ERROR_USE_VERBOSE_NAMES = False, the error key produced by NotFoundError is now automatically converted from CamelCase to snake_case (all lowercase), instead of using the raw Python class name.
Before (v2.22.0):
# settings.py
NINJA_AIO_NOT_FOUND_ERROR_USE_VERBOSE_NAMES = False
raise NotFoundError(BlogPost)
# {"BlogPost": "not found"} ← raw class name
After (v2.23.0):
raise NotFoundError(BlogPost)
# {"blog_post": "not found"} ← snake_case
raise NotFoundError(TestModelSerializer)
# {"test_model_serializer": "not found"}
This ensures the error key is consistent with standard JSON conventions and matches the format already used by the default verbose_name mode.
Implementation:
| File | Change |
|---|---|
ninja_aio/exceptions.py |
model.__name__ converted via re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower() |
🧪 Tests
ExceptionsAndAPITestCase — updated
| Test | Verifies |
|---|---|
test_not_found_error_class_name_mode |
✅ use_verbose_name=False produces snake_case key |
SubclassesTestCase — updated
| Test | Verifies |
|---|---|
test_not_found_error_use_class_name |
✅ use_verbose_name=False key matches snake_case(__name__) |
🎯 Summary
Version 2.23.0 refines the use_verbose_name=False behaviour introduced in 2.22.0. The error key is now always snake_case, making it consistent with both the default verbose-name format and standard JSON naming conventions.
Key benefits:
- 🐍 Consistent casing — both modes now produce snake_case error keys
- ✅ Backwards-compatible — only affects the use_verbose_name=False opt-in mode
v2.22.0
Feb 23, 2026📋 Release Notes
🏷️ [v2.22.0] - 2026-02-23
✨ New Features
🔧 Configurable NotFoundError Key Format
ninja_aio/exceptions.py
NotFoundError now supports a configurable error key format via the Django setting NINJA_AIO_NOT_FOUND_ERROR_USE_VERBOSE_NAMES.
By default (True), the error key continues to use the model's verbose_name with spaces replaced by underscores — preserving full backwards compatibility.
When set to False, the error key uses the Python model class name (model.__name__) instead, which is useful when verbose names contain spaces that are undesirable in JSON keys or when a more Pythonic identifier is preferred.
Default behaviour (unchanged):
# Model with verbose_name = "blog post"
raise NotFoundError(BlogPost)
# {"blog_post": "not found"}
Class name mode:
# settings.py
NINJA_AIO_NOT_FOUND_ERROR_USE_VERBOSE_NAMES = False
raise NotFoundError(BlogPost)
# {"BlogPost": "not found"}
Setting reference:
| Setting | Type | Default | Description |
|---|---|---|---|
NINJA_AIO_NOT_FOUND_ERROR_USE_VERBOSE_NAMES |
bool |
True |
Controls whether NotFoundError uses verbose_name (with _) or __name__ as the error key |
Implementation details:
| File | Change |
|---|---|
ninja_aio/exceptions.py |
Added use_verbose_name class attribute on NotFoundError, reads from settings.NINJA_AIO_NOT_FOUND_ERROR_USE_VERBOSE_NAMES |
📚 Documentation
- Added
docs/api/exceptions.md— full reference for all exception classes (BaseException,SerializeError,AuthError,NotFoundError,PydanticValidationError) and exception handlers - Added Exceptions entry to the API Reference section in
mkdocs.yml
🧪 Tests
ExceptionsAndAPITestCase — 1 new test
Configurable key format:
| Test | Verifies |
|---|---|
test_not_found_error_class_name_mode |
✅ use_verbose_name=False uses model.__name__ as error key |
SubclassesTestCase — 2 new tests
| Test | Verifies |
|---|---|
test_not_found_error_use_class_name |
✅ use_verbose_name=False produces class-name key |
test_not_found_error_use_verbose_name_true |
✅ use_verbose_name=True produces verbose_name-based key |
🎯 Summary
Version 2.22.0 adds fine-grained control over how NotFoundError formats its JSON error key. The new NINJA_AIO_NOT_FOUND_ERROR_USE_VERBOSE_NAMES setting is fully backwards-compatible — existing projects are unaffected unless they opt in.
Key benefits:
- 🔧 Configurable — choose between verbose_name (default) or __name__ as the not-found error key
- ✅ Backwards-compatible — default behaviour is identical to v2.21.0
- 📚 Documented — new dedicated Exceptions API reference page
v2.21.0
Feb 10, 2026📋 Release Notes
🏷️ [v2.21.0] - 2026-02-10
✨ New Features
🔎 Django Q Object Support in Query Schemas and Filters
ninja_aio/schemas/helpers.py,ninja_aio/schemas/filters.py,ninja_aio/models/utils.py,ninja_aio/views/mixins.py
ObjectQuerySchema, ObjectsQuerySchema, and QuerySchema now accept Django Q objects in their filters and getters fields, enabling complex query expressions with OR/AND logic.
Q objects in filters (list operations):
from django.db.models import Q
from ninja_aio.schemas.helpers import ObjectsQuerySchema
# Complex OR conditions
qs = await model_util.get_objects(
request,
query_data=ObjectsQuerySchema(
filters=Q(status="published") | Q(featured=True),
),
)
Q objects in getters (single object retrieval):
from ninja_aio.schemas.helpers import ObjectQuerySchema
obj = await model_util.get_object(
request,
pk=42,
query_data=ObjectQuerySchema(
getters=Q(is_active=True) & Q(role="admin"),
),
)
Q objects in MatchCaseFilterViewSetMixin:
from django.db.models import Q
from ninja_aio.schemas import MatchCaseFilterSchema, MatchConditionFilterSchema, BooleanMatchFilterSchema
class ArticleViewSet(MatchCaseFilterViewSetMixin, APIViewSet):
filters_match_cases = [
MatchCaseFilterSchema(
query_param="is_featured",
cases=BooleanMatchFilterSchema(
true=MatchConditionFilterSchema(
query_filter=Q(status="published") & Q(priority__gte=5),
include=True,
),
false=MatchConditionFilterSchema(
query_filter=Q(status="published") & Q(priority__gte=5),
include=False,
),
),
),
]
Implementation details:
| File | Changes |
|---|---|
ninja_aio/schemas/helpers.py |
filters and getters accept dict \| Q, added ConfigDict(arbitrary_types_allowed=True) |
ninja_aio/schemas/filters.py |
MatchConditionFilterSchema.query_filter accepts dict \| Q, added ConfigDict(arbitrary_types_allowed=True) |
ninja_aio/models/utils.py |
_get_base_queryset() and get_object() handle Q with isinstance check |
ninja_aio/views/mixins.py |
MatchCaseFilterViewSetMixin applies Q objects directly via filter()/exclude() |
📚 Documentation
- Updated
docs/api/models/model_util.mdwith Q object examples forget_objects()andget_object() - Updated
docs/api/views/mixins.mdwith Q object example forMatchCaseFilterViewSetMixin
🧪 Tests
MatchCaseQFilterViewSetMixinTestCase — 3 tests
Q objects in MatchCaseFilterViewSetMixin:
| Test | Verifies |
|---|---|
test_match_case_q_filter_true_includes |
✅ Q object filter with include=True returns matching records |
test_match_case_q_filter_false_excludes |
✅ Q object filter with include=False excludes matching records |
test_match_case_q_filter_no_param_returns_all |
✅ No filter param returns all records |
MatchCaseQExcludeFilterViewSetMixinTestCase — 2 tests
Q objects with exclude behavior:
| Test | Verifies |
|---|---|
test_match_case_q_exclude_true |
✅ Q object exclude with True value excludes matching records |
test_match_case_q_exclude_false_includes_only |
✅ Q object exclude with False value includes only matching records |
ModelUtilQObjectFiltersTestCase — 5 tests
Q objects in ModelUtil filters and getters:
| Test | Verifies |
|---|---|
test_get_objects_with_q_filter |
✅ _get_base_queryset applies Q filter correctly |
test_get_objects_with_q_filter_or |
✅ Q filter with OR logic returns multiple matches |
test_get_object_with_q_getter |
✅ get_object applies Q getter with pk |
test_get_object_with_q_getter_no_pk |
✅ get_object applies Q getter without pk |
test_get_object_with_q_getter_not_found |
✅ get_object raises NotFoundError when Q getter has no match |
New test viewsets:
| File | Addition |
|---|---|
tests/test_app/views.py |
TestModelSerializerMatchCaseQFilterAPI — MatchCaseFilter with Q objects |
tests/test_app/views.py |
TestModelSerializerMatchCaseQExcludeFilterAPI — MatchCaseFilter with Q exclude |
🎯 Summary
Version 2.21.0 adds Django Q object support across query schemas, filters, and match-case mixins, enabling complex OR/AND query expressions without writing custom queryset logic.
Key benefits:
- 🔎 Q Object Support — Use Django Q objects for complex OR/AND queries in filters, getters, and match-case filters
- 🎯 Zero Breaking Changes — Existing dict-based filters continue to work unchanged
- ⚡ Zero Runtime Cost — Q objects are passed directly to Django ORM with no overhead
v2.20.0
Feb 09, 2026📋 Release Notes
🏷️ [v2.20.0] - 2026-02-09
✨ New Features
🔒 Generic Type System for Full Type Safety
ninja_aio/models/utils.py,ninja_aio/models/serializers.py,ninja_aio/views/api.py,ninja_aio/views/mixins.py,ninja_aio/api.py
The entire framework is now fully generic, providing complete IDE autocomplete and static type checking for all CRUD operations. When you specify model type parameters, type checkers (mypy, pyright, pylance) understand exactly which model types are being used.
Generic Serializer[ModelT] — Type-safe CRUD methods:
from ninja_aio.models.serializers import Serializer, SchemaModelConfig
from myapp.models import Book
class BookSerializer(Serializer[Book]): # 👈 Specify model type
class Meta:
model = Book
schema_in = SchemaModelConfig(fields=["title", "author"])
schema_out = SchemaModelConfig(fields=["id", "title", "author"])
# All methods are now properly typed!
serializer = BookSerializer()
book: Book = await serializer.create({"title": "1984"}) # ✅ Returns Book
book: Book = await serializer.save(book) # ✅ Accepts/returns Book
data: dict = await serializer.model_dump(book) # ✅ Accepts Book
Generic APIViewSet[ModelT] — Type-safe model_util access:
from ninja_aio.views import APIViewSet
from ninja_aio.api import NinjaAIO
api = NinjaAIO()
@api.viewset(Book)
class BookAPI(APIViewSet[Book]): # 👈 Explicitly typed
async def my_method(self, request):
# self.model_util is typed as ModelUtil[Book]
book: Book = await self.model_util.get_object(request, pk=1)
print(book.title) # ✅ IDE autocomplete works!
Generic ModelUtil[ModelT] — Automatic type inference:
from ninja_aio.models.utils import ModelUtil
# Type automatically inferred as ModelUtil[Book]
util = ModelUtil(Book)
book: Book = await util.get_object(request, pk=1) # ✅ Returns Book
books: QuerySet[Book] = await util.get_objects(request) # ✅ Returns QuerySet[Book]
Generic Mixins — All filter mixins are now generic:
from ninja_aio.views.mixins import IcontainsFilterViewSetMixin
@api.viewset(Author)
class AuthorAPI(IcontainsFilterViewSetMixin[Author]): # 👈 Specify type
query_params = {"name": (str, None)}
async def custom_method(self, request):
author: Author = await self.model_util.get_object(request, pk=1)
print(author.name) # ✅ Autocomplete works!
Key benefits:
- ✅ IDE Autocomplete — Your IDE suggests correct model fields and methods
- ✅ Type Checking — Type checkers catch errors at development time
- ✅ Better Refactoring — Renaming fields or changing types is caught automatically
- ✅ Zero Runtime Overhead — Generic types are erased at runtime
Implementation details:
| File | Changes |
|---|---|
ninja_aio/models/utils.py |
ModelUtil → ModelUtil(Generic[ModelT]), all methods typed with ModelT |
ninja_aio/models/serializers.py |
Serializer → Serializer(Generic[ModelT]), CRUD methods return/accept ModelT |
ninja_aio/views/api.py |
APIViewSet → APIViewSet(Generic[ModelT]), model_util typed as ModelUtil[ModelT] |
ninja_aio/views/mixins.py |
All mixins → Mixin(APIViewSet[ModelT]) |
ninja_aio/api.py |
viewset() decorator preserves ViewSet type via ViewSetT TypeVar |
Type Variable definitions:
# Consistent across all modules
ModelT = TypeVar("ModelT", bound=models.Model)
ViewSetT = TypeVar("ViewSetT", bound=APIViewSet) # api.py only
Updated docstrings:
All generic classes now include comprehensive type safety examples in their docstrings showing:
- How to specify type parameters
- Expected return types for all methods
- IDE autocomplete behavior
- Type inference patterns
🔍 Field Change Detection Method
ninja_aio/models/serializers.py
Added has_changed(instance, field) method to Serializer class for detecting if a model field has changed compared to its persisted database value.
@api.viewset(Article)
class ArticleViewSet(APIViewSet):
serializer_class = ArticleSerializer
async def custom_update(self, request, pk: int, data):
article = await Article.objects.aget(pk=pk)
article.title = data.title
# Check if title changed before sending notification
if self.serializer.has_changed(article, "title"):
await send_notification(f"Title updated: {article.title}")
await article.asave()
return await self.serializer.model_dump(article)
Use cases:
- 🔔 Conditional notifications (only notify if a specific field changed)
- 📝 Audit logging (track which fields were modified)
- ✅ Validation (enforce business rules based on field changes)
- 🗄️ Caching (invalidate cache only when relevant fields change)
Behavior:
- Returns True if in-memory value differs from DB value
- Returns False for new instances (those without a primary key)
- Performs a targeted query: .filter(pk=pk).values(field).get()[field]
📤 Custom Schema Parameter for Serialization Methods
ninja_aio/models/serializers.py
Both model_dump() and models_dump() now accept an optional schema parameter, allowing you to specify a custom schema for serialization instead of using the default (detail or read schema).
model_dump(instance, schema=None) — Serialize single instance:
# Use default schema (detail schema if defined, otherwise read schema)
data = await serializer.model_dump(article)
# Use a specific custom schema
custom_schema = ArticleSerializer.generate_read_s()
data = await serializer.model_dump(article, schema=custom_schema)
models_dump(instances, schema=None) — Serialize multiple instances:
# Use default schema
articles = Article.objects.all()
data = await serializer.models_dump(articles)
# Use a specific custom schema
custom_schema = ArticleSerializer.generate_read_s()
data = await serializer.models_dump(articles, schema=custom_schema)
New internal method:
| Method | Description |
|---|---|
_get_dump_schema(schema=None) |
🎯 Returns provided schema, or falls back to detail schema → read schema |
Use cases:
- 🎨 Different response formats for the same endpoint
- 📊 Custom schemas for exports (CSV, Excel, PDF)
- 🔐 Role-based field visibility (admin vs user schemas)
- ⚡ Performance optimization (minimal schemas for bulk operations)
📚 Documentation
🆕 Type Hints & Type Safety Documentation
docs/api/type_hints.md(NEW)
Created comprehensive documentation covering the new generic type system:
Sections:
- 📖 Overview — Benefits of type safety (autocomplete, type checking, refactoring, zero overhead)
- 🔧 Generic Serializer — Basic usage, benefits, and examples
- 🎯 Generic APIViewSet — Three approaches: Type the ViewSet, Type the Serializer (recommended), or both
- 🛠️ Generic ModelUtil — Automatic type inference examples
- 🔌 Generic Mixins — All six filter mixins with type parameters
- ❓ Why Explicit Type Parameters? — Python's type system limitations explained
- 📊 Framework Comparison — Django Stubs, FastAPI, SQLAlchemy patterns
- ⚙️ Type Checker Configuration — Setup for VS Code (Pylance), PyCharm, mypy
- 🐛 Troubleshooting — Common issues and solutions
- 📋 Summary Table — Quick reference for all usage patterns
Added to mkdocs navigation:
- API Reference:
- Type Hints & Type Safety: api/type_hints.md # 👈 First item
- Views: ...
📝 Serializer Documentation Updates
docs/api/models/serializers.md
Added three new sections to document the latest Serializer improvements:
1. Serialization Methods — Documents model_dump() and models_dump() with optional schema parameter:
- Default schema usage (detail → read fallback)
- Custom schema usage examples
- Type hints showing proper typing
2. Field Change Detection — Documents has_changed() method:
- Practical example with conditional notifications
- Four key use cases (notifications, audit logging, validation, caching)
- Behavior note for new instances
3. Type Safety Integration — Updated Generic Serializer section to show:
- Optional custom schema usage in type hints
- Integration with typed CRUD methods
🏠 README Updates
README.md
Added Type Safety as the first feature in the features table:
| Feature | Technology | Description |
|---|---|---|
| 🔒 Type Safety | Generic classes | Full IDE autocomplete and type checking with generic ModelUtil, Serializer, and APIViewSet |
🎯 Summary
Version 2.20.0 introduces comprehensive type safety across the entire framework through generic classes, bringing django-ninja-aio-crud on par with modern Python frameworks in terms of static type analysis support.
Key benefits:
- 🎯 Zero Breaking Changes — All existing code continues to work without modification
- 🔒 Type Safety — Full IDE autocomplete and type checking when you specify type parameters
- 📚 Documentation — Comprehensive guide covering all type safety patterns
- 🛠️ Enhanced Serializers — Field change detection and flexible schema dumping
- ⚡ Zero Runtime Cost — Generic types are erased at runtime, no performance impact
Three typing approaches:
1. Type the Serializer (Recommended) — Type once, all serializer methods typed
2. Type the ViewSet — For model_util-heavy code
3. Type both — Maximum type safety everywhere
The framework now provides the same level of type safety as Django Stubs, FastAPI, and SQLAlchemy 2.0 while maintaining its async-first design and zero-boilerplate philosophy.
v2.19.0
Feb 04, 2026📋 Release Notes
🏷️ [v2.19.0] - 2026-02-04
✨ New Features
🔧 Schema Method Overrides on Serializer Inner Classes
ninja_aio/models/serializers.py
You can now define Pydantic schema method overrides (e.g., model_dump, model_validate, custom properties) on serializer inner classes. The framework automatically injects these methods into the generated Pydantic schema subclass, with full super() support via __class__ cell rebinding.
ModelSerializer — define on inner serializer classes:
from __future__ import annotations
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from ninja import Schema
class MyModel(ModelSerializer):
name = models.CharField(max_length=255)
class ReadSerializer:
fields = ["id", "name"]
def model_dump(
self: Schema,
*,
mode: str = "python",
include: Any = None,
exclude: Any = None,
context: Any = None,
by_alias: bool = False,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
round_trip: bool = False,
warnings: bool | str = True,
serialize_as_any: bool = False,
) -> dict[str, Any]:
data = super().model_dump(
mode=mode, include=include, exclude=exclude,
context=context, by_alias=by_alias,
exclude_unset=exclude_unset, exclude_defaults=exclude_defaults,
exclude_none=exclude_none, round_trip=round_trip,
warnings=warnings, serialize_as_any=serialize_as_any,
)
data["name"] = data["name"].upper()
return data
Serializer (Meta-driven) — define on validator inner classes:
class MySerializer(serializers.Serializer):
class Meta:
model = MyModel
schema_out = serializers.SchemaModelConfig(fields=["id", "name"])
class ReadValidators:
def model_dump(self: Schema, **kwargs) -> dict[str, Any]:
data = super().model_dump(**kwargs)
data["name"] = data["name"].upper()
return data
New core methods on BaseSerializer:
| Method | Description |
|---|---|
_collect_schema_overrides(source_class) |
🔍 Scans a class for regular callables that aren't validators, config attrs, or dunders |
_get_schema_overrides(schema_type) |
🗺️ Maps schema types to their override source class (overridden per serializer) |
Implementation details:
- Overrides are collected alongside validators during schema generation
- __class__ cell rebinding via types.FunctionType + types.CellType ensures bare super() resolves to the correct subclass
- Validators, model_config, and method overrides coexist on the same inner class
- _CONFIG_ATTRS frozenset filters out configuration attributes (fields, customs, optionals, excludes, relations_as_id, model_config)
⚙️ Pydantic model_config Support on Serializers
ninja_aio/models/serializers.py
Both serializer patterns now support applying Pydantic ConfigDict to generated schemas.
ModelSerializer — via model_config attribute:
from pydantic import ConfigDict
class MyModel(ModelSerializer):
name = models.CharField(max_length=255)
class CreateSerializer:
fields = ["name"]
model_config = ConfigDict(str_strip_whitespace=True)
Serializer (Meta-driven) — via model_config_override in SchemaModelConfig:
class MySerializer(serializers.Serializer):
class Meta:
model = MyModel
schema_in = serializers.SchemaModelConfig(
fields=["name"],
model_config_override=ConfigDict(str_strip_whitespace=True),
)
New core methods on BaseSerializer:
| Method | Description |
|---|---|
_get_model_config(schema_type) |
Returns ConfigDict for the given schema type |
New field on SchemaModelConfig:
| Field | Type | Description |
|---|---|---|
model_config_override |
Optional[dict] |
Pydantic ConfigDict to apply to the generated schema |
🔬 Framework Comparison Benchmark Suite
tests/comparison/
Added a comprehensive benchmark suite comparing django-ninja-aio-crud against other popular Python REST frameworks using the same Django models and database.
Compared frameworks:
- 🟣 django-ninja-aio-crud — Native async CRUD automation
- 🔵 Django Ninja (pure) — Async-ready, manual endpoint definition
- 🟠 ADRF — Async Django REST Framework
- 🟢 FastAPI — Native async, Starlette-based
Operations tested: create, list, retrieve, update, delete, filter, relation serialization, bulk serialization (100 & 500 items)
New files:
| File | Description |
|---|---|
tests/comparison/base.py |
Base benchmark test class |
tests/comparison/test_comparison.py |
Comparison benchmark tests |
tests/comparison/frameworks/ |
Framework-specific implementations (ninja_aio, ninja, adrf, fastapi) |
tests/comparison/generate_report.py |
Interactive HTML report generator |
tests/comparison/generate_markdown.py |
Markdown report generator |
run-comparison.sh |
Helper script to run benchmarks and generate reports |
📊 Performance Analysis Tools
tests/performance/tools/
Added statistical analysis tools for detecting performance regressions and analyzing benchmark stability.
| Tool | Description |
|---|---|
detect_regression.py |
Statistical regression detection with σ significance (CI/CD recommended) |
analyze_perf.py |
Quick overview of recent benchmark runs |
analyze_variance.py |
Benchmark stability and coefficient of variation analysis |
compare_days.py |
Day-over-day performance comparison |
check-performance.sh |
Helper script for running all analysis tools |
🔧 Improvements
📱 Mobile Chart Fix in Reports
tests/performance/generate_report.py,tests/comparison/generate_report.py
Fixed Chart.js charts rendering incorrectly on mobile viewports by adding maintainAspectRatio: false to all chart configurations, allowing charts to properly respect their container's CSS height constraints.
🎨 Enhanced HTML Report Generation
tests/comparison/generate_report.py,tests/performance/generate_report.py
- 🏆 Winner highlighting in comparison tables with purple accent
- 🌗 Light/dark mode support via
prefers-color-scheme - 📱 Responsive design with mobile breakpoints (768px, 480px)
- 📈 Interactive Chart.js bar and trend charts
📚 Documentation
Updated documentation for model_config, schema method overrides, and self: Schema typing pattern across model serializer, serializer, and validators docs. Added Pydantic ConfigDict and BaseModel API reference links. Added warning about no automatic argument hinting on inner classes. Updated deployment, troubleshooting, and contributing guides. Rebranded all references from "Django Ninja Aio CRUD" to "Django Ninja AIO".
🧪 Tests
ModelSerializerSchemaOverridesTestCase — 3 tests
Category: Schema method override verification (ModelSerializer)
| Test | Verifies |
|---|---|
test_model_dump_override_applied |
✅ model_dump override transforms output correctly |
test_super_call_works |
✅ Bare super() resolves correctly in injected methods |
test_model_dump_kwargs_passthrough |
✅ All model_dump kwargs are forwarded properly |
MetaSerializerSchemaOverridesTestCase — 2 tests
Category: Schema method override verification (Meta-driven Serializer)
| Test | Verifies |
|---|---|
test_model_dump_override_applied |
✅ model_dump override transforms output on Meta-driven Serializer |
test_super_call_works |
✅ Bare super() resolves correctly in Meta-driven overrides |
CollectSchemaOverridesTestCase — 6 tests
Category: _collect_schema_overrides unit tests
| Test | Verifies |
|---|---|
test_collects_regular_methods |
✅ Regular methods are collected |
test_skips_validators |
✅ PydanticDescriptorProxy instances are skipped |
test_skips_config_attrs |
✅ Config attributes (fields, customs, etc.) are skipped |
test_skips_dunders |
✅ Dunder methods are skipped |
test_returns_empty_for_none |
✅ Returns empty dict for None input |
test_collects_staticmethod_classmethod |
✅ Static and class methods are collected |
BaseSerializerSchemaOverridesDefaultTestCase — 2 tests
Category: Default behavior and override-only application
| Test | Verifies |
|---|---|
test_default_returns_empty |
✅ Base _get_schema_overrides returns empty dict |
test_apply_overrides_only |
✅ Overrides work without validators |
ModelConfigTestCase — 10 tests
Category: Pydantic model_config / model_config_override support
| Test | Verifies |
|---|---|
test_model_config_* |
✅ ConfigDict applied to ModelSerializer schemas (create/read/update) |
test_meta_model_config_override_* |
✅ ConfigDict applied to Meta-driven Serializer schemas |
test_str_strip_whitespace |
✅ Whitespace stripping works end-to-end |
New test fixtures:
| File | Addition |
|---|---|
tests/test_app/models.py |
TestModelWithSchemaOverrides — ModelSerializer with model_dump override on ReadSerializer |
tests/test_app/serializers.py |
TestModelWithSchemaOverridesMetaSerializer — Serializer with model_dump override on ReadValidators |
tests/test_app/serializers.py |
TestModelWithModelConfigMetaSerializer — Serializer with model_config_override on all schemas |
Test results:
- ✅ 656 tests pass
- ✅ 99% coverage on ninja_aio/models/serializers.py
🎯 Summary
Django Ninja AIO v2.19.0 introduces two major serializer features: schema method overrides and Pydantic model_config support. Schema method overrides let you inject custom methods (like model_dump) into generated Pydantic schemas from inner serializer classes, with full super() support via __class__ cell rebinding. Pydantic ConfigDict can now be applied per-schema for configuration like str_strip_whitespace. This release also adds a framework comparison benchmark suite and statistical performance analysis tools.
Key benefits:
- 🔧 Schema Method Overrides — Inject custom model_dump, model_validate, or any method into generated schemas with bare super() support
- ⚙️ Pydantic ConfigDict — Apply model_config per-schema on both ModelSerializer and Meta-driven Serializer
- 🔬 Framework Comparison — Benchmark against Django Ninja, ADRF, and FastAPI with interactive HTML reports
- 📊 Regression Detection — Statistical tools for detecting performance regressions in CI/CD
- 📱 Mobile-Fixed Charts — Chart.js charts render correctly on mobile viewports
- 🧪 23 New Tests — Comprehensive coverage for overrides, model_config, and edge cases
- 🔄 Backward Compatible — All changes are additive with no breaking changes
v2.18.3
Feb 02, 2026📋 Release Notes
🏷️ [v2.18.3] - 2026-02-02
⚡ Performance Improvements
🚀 Foreign Key Resolution Optimization
ninja_aio/models/utils.py
Eliminated redundant database queries during create and update operations by optimizing how foreign key relationships are loaded after object persistence.
The Problem:
When creating or updating objects with foreign key fields, the framework was fetching FK relationships twice:
1. Once in _resolve_fk() to convert FK IDs to model instances (required by Django's ORM)
2. Again in get_object() with select_related when retrieving the created/updated object
Example of redundancy:
# User creates: POST {"name": "Article", "author_id": 5}
# Before optimization:
# Query 1: SELECT * FROM author WHERE id = 5 (_resolve_fk)
# Query 2: INSERT INTO article (name, author_id) VALUES (...)
# Query 3: SELECT * FROM article
# LEFT JOIN author ON ...
# WHERE id = 123 (get_object - redundant!)
# After optimization:
# Query 1: SELECT * FROM author WHERE id = 5 (_resolve_fk)
# Query 2: INSERT INTO article (name, author_id) VALUES (...)
# Query 3: SELECT * FROM article WHERE id = 123 (prefetch reverse relations only)
# # FK already in memory, not re-fetched!
New method:
| Method | Line | Description |
|---|---|---|
_prefetch_reverse_relations_on_instance() |
645-689 | Prefetches only reverse relations (reverse FK, reverse O2O, M2M) on an existing instance without re-fetching forward FKs |
How it works:
- No reverse relations → Returns original instance with FK cache intact
- Reverse relations exist → Refetches instance with:
-prefetch_related()for reverse relations
-select_related()for forward FKs to keep them loaded
Modified methods:
| Method | Line | Change |
|---|---|---|
create_s() |
883-899 | Now keeps full object from acreate() instead of just PK; calls _prefetch_reverse_relations_on_instance() instead of get_object() |
update_s() |
1085-1100 | Calls _prefetch_reverse_relations_on_instance() instead of second get_object() after save |
_resolve_fk() |
632-634 | Added None check for nullable FK fields |
Performance impact:
| Operation | Before | After | Queries Saved |
|---|---|---|---|
| Create (with FK, no reverse rels) | FK fetch → Create → Full refetch (FK + reverse) | FK fetch → Create → Return (FK in memory) | 1 FK query ✅ |
| Create (with FK + reverse rels) | FK fetch → Create → Full refetch (FK + reverse) | FK fetch → Create → Refetch (FK + reverse) | 1 FK query ✅ |
| Update (changing FK, no reverse rels) | Full fetch → New FK fetch → Update → Full refetch | Full fetch → New FK fetch → Update → Return (FK in memory) | 1 FK query ✅ |
| Update (changing FK + reverse rels) | Full fetch → New FK fetch → Update → Full refetch | Full fetch → New FK fetch → Update → Refetch (FK + reverse) | 1 FK query ✅ |
Real-world example:
@api.viewset(model=Article)
class ArticleViewSet(APIViewSet):
pass
# POST /articles/
# Payload: {"title": "Django Ninja", "author_id": 5}
#
# Before: 3 queries (2 for author FK - redundant!)
# After: 2 queries (1 for author FK)
#
# Result: 33% fewer queries for create operations with FKs!
Edge case handling:
| Scenario | Behavior |
|---|---|
Nullable FK with None value |
Skips FK resolution (line 632-634) |
| Model with FK but no reverse relations | Returns original instance, no refetch needed |
| Model with FK and reverse relations | Refetches with both select_related and prefetch_related |
| Model without FK fields | No change in behavior |
🧪 Tests
FKOptimizationTestCase — 9 new tests
Test file: tests/models/test_fk_optimization.py (new file, 345 lines)
Category: Functional correctness verification
| Test | Verifies |
|---|---|
test_create_s_with_fk_returns_correct_data |
✅ Create operations with FK fields produce correct results |
test_create_s_fk_instance_attached |
✅ FK instances are accessible in returned data without N+1 queries |
test_update_s_with_fk_change |
✅ Update operations correctly change FK values |
test_update_s_fk_instance_attached |
✅ Updated FK instances are accessible in returned data |
test_create_s_without_fk_still_works |
✅ Models without FK fields continue to work correctly |
test_reverse_relations_loaded_after_create |
✅ Forward FK relationships are properly loaded after create |
test_multiple_creates_with_same_fk |
✅ Repeated creates with same FK value work correctly |
test_parent_model_with_reverse_relations |
✅ Models with reverse relations are handled correctly |
test_update_s_without_changing_fk |
✅ Partial updates that don't change FK fields work correctly |
New test fixtures:
| File | Addition |
|---|---|
tests/test_app/models.py |
Models already existed for FK testing (TestModelSerializerForeignKey, TestModelSerializerReverseForeignKey) |
Test results:
- ✅ 617 tests pass (up from 608)
- ✅ 19 performance tests pass
- ✅ 99% coverage on ninja_aio/models/utils.py (line 686 is defensive code for models with both forward FKs and reverse relations - not exercised by current test suite but important for real-world usage)
🎯 Summary
Django Ninja Aio CRUD v2.18.3 is a performance optimization release that eliminates redundant foreign key queries during create and update operations. By intelligently caching FK instances resolved during input parsing and only refetching reverse relations when necessary, the framework reduces database queries by 33% for typical CRUD operations involving foreign keys. This optimization is completely transparent to end users - no code changes required - while delivering measurable performance improvements for API endpoints with relational data.
Key benefits:
- ⚡ 33% Fewer Queries — One less DB query per create/update operation with foreign keys
- 🎯 Smart Caching — Forward FKs kept in memory after resolution, only reverse relations refetched when needed
- 🔒 Zero Breaking Changes — Completely backward compatible, optimization happens automatically
- 🧪 Thoroughly Tested — 9 new tests covering all FK scenarios and edge cases
- 📊 Performance Benchmarks — All 19 performance tests pass with no regressions
- 💡 Transparent — No code changes needed to benefit from optimization
v2.18.1
Feb 01, 2026📋 Release Notes
🏷️ [v2.18.1] - 2026-02-01
🔒 Security Fixes
🔄 Circular Reference Protection
ninja_aio/models/serializers.py
Fixed potential infinite recursion and stack overflow from circular model relationships by adding thread-safe circular reference detection.
New methods:
| Method | Line | Description |
|---|---|---|
_resolution_context |
1921 | Thread-local storage for resolution stack |
_get_resolution_stack() |
1926-1934 | Returns resolution stack for current thread |
_is_circular_reference() |
1937-1954 | Checks if model/schema_type is already being resolved |
_push_resolution() |
1957-1962 | Pushes model/schema_type onto resolution stack |
_pop_resolution() |
1965-1969 | Pops model/schema_type from resolution stack |
Enhanced method:
- _resolve_related_model_schema() (lines 1994-2039) - Now detects circular references and raises ValueError with clear message
Example scenario that previously caused infinite recursion:
class Author(ModelSerializer):
articles = models.ManyToManyField('Article', related_name='authors')
class ReadSerializer:
fields = ['id', 'name', 'articles']
class Article(ModelSerializer):
authors = models.ManyToManyField(Author, related_name='articles')
class ReadSerializer:
fields = ['id', 'title', 'authors'] # Circular!
Now raises a clear error instead of causing stack overflow.
🛡️ Field Injection Prevention
ninja_aio/models/utils.py
Fixed potential security vulnerability by adding input field validation to prevent malicious field injection in payloads.
New methods:
| Method | Line | Description |
|---|---|---|
get_valid_input_fields() |
2282-2322 | Returns allowlist of valid field names from model |
_validate_input_fields() |
2440-2476 | Validates payload fields against model, raises ValueError for invalid fields |
Applied in:
- parse_input_data() (line 908) - Validates all input payloads before processing
Now blocks malicious payloads:
{
"username": "hacker",
"password": "secret",
"_state": {}, # ❌ Now blocked
"pk": 999, # ❌ Now blocked if not in model fields
}
🔍 Filter Field Validation
ninja_aio/views/api.py
Fixed potential filter injection vulnerability by adding comprehensive filter field validation.
New validation methods:
| Method | Line | Description |
|---|---|---|
_validate_filter_field() |
2749-2840 | Main validation method for filter field paths |
_is_lookup_suffix() |
Helper | Checks if suffix is valid Django lookup (e.g., __icontains, __gte) |
_get_related_model() |
Helper | Extracts related model from ForeignKey/ManyToMany field |
_validate_non_relation_field() |
Helper | Validates non-relation field placement in path |
Applied to all filter mixins:
- IcontainsFilterViewSetMixin (lines 2886-2904)
- BooleanFilterViewSetMixin (lines 2907-2920)
- NumericFilterViewSetMixin (lines 2923-2936)
- DateFilterViewSetMixin (lines 2939-2952)
- RelationFilterViewSetMixin (lines 2955-2968)
- MatchCaseFilterViewSetMixin (lines 2971-2984)
Now blocks injection attempts:
?author___state__db=malicious # ❌ Now blocked (invalid lookup)
?author__password__icontains=admin # ❌ Now blocked (invalid field path)
🎯 Django Lookup Types
ninja_aio/types.py
Added DjangoLookup type and VALID_DJANGO_LOOKUPS set containing all 36 valid Django ORM lookup suffixes for validation.
Valid lookups:
- Equality: exact, iexact
- Comparison: gt, gte, lt, lte, range
- Text: contains, icontains, startswith, istartswith, endswith, iendswith, regex, iregex
- Boolean: isnull, in
- Date/Time: date, year, month, day, week, week_day, quarter, time, hour, minute, second
🚀 Performance Improvements
⚡ Schema Generation Caching
ninja_aio/models/serializers.py
Added @lru_cache(maxsize=128) to all schema generation methods, dramatically reducing repeated schema generation overhead.
Cached methods:
| Method | Line | Expected Speedup |
|---|---|---|
generate_read_s() |
1193 | 10-100x for repeated calls |
generate_detail_s() |
1207 | 10-100x for repeated calls |
generate_create_s() |
1225 | 10-100x for repeated calls |
generate_update_s() |
1238 | 10-100x for repeated calls |
generate_related_s() |
1252 | 10-100x for repeated calls |
Benefit: Schema generation is expensive (Pydantic model creation, validator collection, etc.). Since model structure is static, caching eliminates redundant work.
⚡ Relation Discovery Caching
ninja_aio/models/utils.py
Added class-level _relation_cache dictionary to cache discovered model relationships.
Cached methods:
| Method | Line | What It Caches |
|---|---|---|
get_reverse_relations() |
2575-2361 | Reverse ForeignKey and ManyToMany relations |
get_select_relateds() |
2621-2640 | Forward ForeignKey relations for select_related |
Benefit: Model relationships are static at runtime. Caching eliminates repeated model introspection overhead.
⚡ Parallel Field Processing
ninja_aio/models/utils.py
Refactored payload processing to use asyncio.gather() for parallel field resolution.
New method:
- _process_payload_fields() (lines 2546-2578) - Processes all fields in parallel
Applied in:
- parse_input_data() (lines 915-916) - Fetches all field objects and resolves all FK fields concurrently
Benefit: Significantly faster for payloads with multiple fields, especially when resolving foreign keys that require database lookups.
🧹 Code Quality Improvements
Reduced Cognitive Complexity in BaseSerializer
ninja_aio/models/serializers.py
Extracted helper methods from _generate_model_schema() to improve readability and maintainability.
New helper methods:
| Method | Line | Purpose |
|---|---|---|
_create_out_or_detail_schema() |
1092-1114 | Handles Out and Detail schema types |
_create_related_schema() |
1117-1132 | Handles Related schema type |
_create_in_or_patch_schema() |
1135-1147 | Handles In and Patch schema types |
Simplified main method:
- _generate_model_schema() (lines 1150-1184) - Now dispatches to appropriate helper based on schema type
Benefit: Reduced cognitive complexity, improved testability, clearer error handling paths.
Reduced Cognitive Complexity in ModelUtil
ninja_aio/models/utils.py
Extracted helper methods from parse_input_data() to improve readability and testability.
New helper methods:
| Method | Line | Purpose |
|---|---|---|
_collect_custom_and_optional_fields() |
2478-2514 | Collects custom and optional fields from payload |
_determine_skip_keys() |
2516-2545 | Determines which keys to skip during processing |
_process_payload_fields() |
2546-2578 | Processes payload fields in parallel |
Added type hints and docstrings:
| Method | Line | Return Type |
|---|---|---|
_get_field() |
2640-2648 | models.Field |
_decode_binary() |
2650-2658 | None |
_resolve_fk() |
2660-2668 | None |
_bump_object_from_schema() |
2670-2675 | dict |
_validate_read_params() |
2677-2682 | None |
Type Hints & Documentation in ViewSets
ninja_aio/views/api.py
Added comprehensive return type hints to all view registration and authentication methods.
Updated methods:
| Method | Line | Return Type |
|---|---|---|
_add_views() |
2724-2739 | Router |
add_views_to_route() |
2846-2862 | Router |
views() |
— | None |
get_view_auth() |
— | list \| None |
post_view_auth() |
— | list \| None |
put_view_auth() |
— | list \| None |
patch_view_auth() |
— | list \| None |
delete_view_auth() |
— | list \| None |
_generate_path_schema() |
— | Schema |
📚 Documentation Improvements
📱 Mobile Responsiveness
docs/extra.css
Added comprehensive mobile responsive CSS for better documentation experience on mobile devices.
Improvements:
- 📱 Hero section optimized for small screens with reduced logo size (280px on mobile, 240px on very small screens)
- 🎯 Responsive badge layout with proper wrapping and flexbox (badges reduced to 20px height on mobile)
- 📱 Mobile-friendly CTA buttons with proper touch targets (44px minimum)
- 📊 Responsive grid cards (single column on mobile)
- 📝 Better code block overflow handling
- 📋 Responsive tables with horizontal scroll
- 🎨 Optimized release cards and timeline for mobile
- 📐 Smaller fonts and tighter spacing for mobile (768px and 480px breakpoints)
- 🔤 Announcement bar with proper padding to prevent text cutoff
- 🖼️ Header logo reduced from 2.0rem to 1.6rem on mobile devices
Updated Tutorial Documentation
Updated all tutorial and API documentation to use the @api.viewset() decorator pattern:
| File | What Changed |
|---|---|
docs/tutorial/crud.md |
Simplified viewset registration examples |
docs/tutorial/authentication.md |
Updated authentication examples |
docs/tutorial/filtering.md |
Updated all viewset examples |
docs/api/authentication.md |
Updated authentication examples |
docs/api/pagination.md |
Updated pagination examples |
Before:
class ArticleViewSet(APIViewSet):
model = Article
api = api
ArticleViewSet().add_views_to_route()
After (cleaner):
@api.viewset(model=Article)
class ArticleViewSet(APIViewSet):
pass
Updated README and Documentation
README.md,docs/index.md
- ✅ Updated to use full logo image (
logo-full.png) - ✅ Added Performance badge and link to benchmarks
- ✅ Improved landing page structure
- ✅ Better mobile responsiveness
Updated Project Instructions
CLAUDE.md
New sections:
- 🧪 Running Performance Tests - Guide to running and understanding performance benchmarks (for contributors)
- ✅ Test-Driven Development Protocol - Testing requirements for all code changes
- 📦 Import Style Guideline - PEP 8 import placement requirements
Improvements:
- 🗑️ Removed "All Files Changed" table requirement from changelog format
- ✨ Streamlined changelog guidelines
🧪 Test Coverage
Added comprehensive tests for all new functionality:
tests/models/test_models_extra.py — 161 new lines:
| Test Case | Tests | Verifies |
|---|---|---|
ModelUtilSerializerReadOptimizationsTestCase |
2 | Queryset optimization for serializer reads |
ModelUtilHelperMethodsTestCase |
9 | Refactored helper methods |
- test_validate_input_fields_* |
3 | Field injection prevention |
- test_collect_custom_and_optional_fields_* |
4 | Custom/optional field collection |
- test_determine_skip_keys_* |
2 | Skip key determination logic |
tests/test_serializers.py — 309 new lines, 14 test cases:
| Test Case | Tests | Verifies |
|---|---|---|
BaseSerializerDefaultMethodsTestCase |
2 | Default method implementations |
ResolveSerializerReferenceEdgeCasesTestCase |
3 | Circular reference detection edge cases |
GetSchemaOutDataEdgeCasesTestCase |
1 | Schema output data edge cases |
GenerateModelSchemaEdgeCasesTestCase |
2 | Schema generation edge cases |
GetRelatedSchemaDataEdgeCasesTestCase |
1 | Related schema data edge cases |
QuerysetRequestNotImplementedTestCase |
1 | NotImplementedError for missing queryset_request |
ModelSerializerGetFieldsEdgeCasesTestCase |
1 | Field retrieval edge cases |
SerializerGetSchemaMetaEdgeCasesTestCase |
2 | Schema meta edge cases |
SerializerCRUDMethodsTestCase |
4 | CRUD method edge cases |
WarnMissingRelationSerializerTestCase |
1 | Warning for missing relation serializers |
BuildSchemaReverseRelNoneTestCase |
1 | Reverse relation None handling |
BuildSchemaForwardRelNoReadFieldsTestCase |
1 | Forward relation missing read fields |
tests/views/test_views.py — 237 new lines:
| Test Case | Tests | Verifies |
|---|---|---|
APIViewViewsPassTestCase |
1 | View registration with decorator |
APIViewSetDisableAllTestCase |
1 | Disabling all CRUD operations |
RelationsFiltersFieldsTestCase |
1 | Relation filter field validation |
BuildHandlerTestCase |
2 | Handler building edge cases |
FilterValidationHelpersTestCase |
17 | All filter validation helper methods |
tests/helpers/test_many_to_many_api.py — 31 new lines:
| Test Case | Tests | Verifies |
|---|---|---|
GetApiPathNoSlashTestCase |
1 | API path with append_slash=False |
Total: 50+ new unit tests for security features and edge cases. 100% coverage maintained.
🏗️ Internal/Development Improvements
Performance Benchmark Suite (for contributors)
tests/performance/
Added comprehensive performance benchmarking infrastructure for monitoring framework performance during development.
Benchmark categories:
- Schema generation (4 tests)
- Serialization (4 tests)
- CRUD operations (5 tests)
- Filter performance (6 tests)
Note: This is for development/CI only. End users are not affected.
GitHub Actions Workflow
.github/workflows/performance.yml
Added automated performance benchmarking workflow:
- Runs on push to main and PRs
- Checks for >20% performance regressions
- Deploys interactive reports to GitHub Pages
Gitignore Updates
.gitignore
Added performance report files:
- performance_results.json
- performance_report.html
🎯 Summary
Django Ninja Aio CRUD v2.18.1 is a maintenance release focused on security fixes, performance improvements, and documentation enhancements. Three critical security vulnerabilities have been fixed to protect against circular reference attacks, field injection, and filter injection. Performance improvements through caching and parallel processing deliver 2-10x speedups for schema generation and serialization. Documentation has been enhanced with comprehensive mobile responsiveness. Internal improvements include a performance benchmark suite for ongoing development.
Key benefits:
- 🔒 Security Hardened — Fixed vulnerabilities: circular reference protection, field injection prevention, filter field validation
- ⚡ Faster Performance — 2-10x speedup for schema generation and serialization through caching and parallel processing
- 📱 Mobile-Friendly Docs — Comprehensive mobile responsiveness with optimized layouts and touch targets
- 🧹 Cleaner Code — Reduced cognitive complexity, comprehensive type hints, improved maintainability
- 🧪 Robust Testing — 50+ new unit tests, 100% coverage maintained
- 📊 Performance Monitoring — Internal benchmark suite for ongoing performance tracking (contributors only)
v2.18.0
Feb 01, 2026📋 Release Notes
🏷️ [v2.18.0] - 2026-02-01
✨ New Features
🛡️ Validators on Serializers
ninja_aio/models/serializers.py
Pydantic @field_validator and @model_validator can now be declared directly on serializer configuration classes. The framework automatically collects PydanticDescriptorProxy instances and creates a subclass of the generated schema with the validators attached.
Supported on both serializer patterns:
| Pattern | Where to declare validators |
|---|---|
ModelSerializer |
Inner classes: CreateSerializer, ReadSerializer, UpdateSerializer, DetailSerializer |
Serializer (Meta-driven) |
Dedicated inner classes: CreateValidators, ReadValidators, UpdateValidators, DetailValidators |
🔀 Different validation rules can be applied per operation (e.g., stricter rules on create, lenient on update).
ModelSerializer example:
from django.db import models
from pydantic import field_validator, model_validator
from ninja_aio.models import ModelSerializer
class Book(ModelSerializer):
title = models.CharField(max_length=120)
description = models.TextField(blank=True)
class CreateSerializer:
fields = ["title", "description"]
@field_validator("title")
@classmethod
def validate_title_min_length(cls, v):
if len(v) < 3:
raise ValueError("Title must be at least 3 characters")
return v
class UpdateSerializer:
optionals = [("title", str), ("description", str)]
@field_validator("title")
@classmethod
def validate_title_not_empty(cls, v):
if v is not None and len(v.strip()) == 0:
raise ValueError("Title cannot be blank")
return v
Serializer (Meta-driven) example:
from pydantic import field_validator
from ninja_aio.models import serializers
class BookSerializer(serializers.Serializer):
class Meta:
model = Book
schema_in = serializers.SchemaModelConfig(fields=["title", "description"])
schema_out = serializers.SchemaModelConfig(fields=["id", "title", "description"])
class CreateValidators:
@field_validator("title")
@classmethod
def validate_title_min_length(cls, v):
if len(v) < 3:
raise ValueError("Title must be at least 3 characters")
return v
🧩 New Core Methods on BaseSerializer
ninja_aio/models/serializers.py
| Method | Description |
|---|---|
_collect_validators(source_class) |
🔍 Scans a class for PydanticDescriptorProxy instances created by @field_validator / @model_validator decorators. Returns a dict mapping attribute names to validator proxies. |
_apply_validators(schema, validators) |
🔗 Creates a subclass of the generated schema with validators attached. Pydantic discovers validators during class creation. |
_get_validators(schema_type) |
🗺️ Abstract method for subclasses to map schema types (In, Patch, Out, Detail, Related) to their validator source classes. |
🆕 New _parse_payload() Method on Serializer
ninja_aio/models/serializers.py
Serializer._parse_payload(payload) accepts both dict and Schema instances, automatically calling model_dump() on Schema inputs. This enables passing validated Pydantic schemas directly to create() and update().
📖 New Tutorial: "Define Your Serializer"
docs/tutorial/serializer.md
Comprehensive tutorial page for the Meta-driven Serializer approach as an alternative to Step 1 (ModelSerializer). Covers:
- 📐 Schema definition with
SchemaModelConfig - 🔗 Relationships via
relations_serializers - ⚙️ Custom and computed fields
- 🚀 Query optimizations with
QuerySet - 🔄 Lifecycle hooks
- 🔌 Connecting to
APIViewSet
📚 New Validators Documentation Page
docs/api/models/validators.md
Full dedicated documentation page covering:
- 🏗️
ModelSerializerandSerializerapproaches - ✅ Supported validator types and modes
- 🔀 Different validators per operation
- ⚙️ Internal mechanics
- ⚠️ Error handling (422 responses)
- 💡 Complete examples
🔧 Improvements
⚡ Schema Generation Now Applies Validators
ninja_aio/models/serializers.py
_generate_model_schema() now calls _get_validators() for the requested schema type and _apply_validators() on the resulting schema. Applied consistently across all schema types: Out, Detail, Related, In, and Patch.
📦 create() and update() Accept Schema Objects
ninja_aio/models/serializers.py
Serializer.create() and Serializer.update() payload parameter type changed from dict[str, Any] to dict[str, Any] | Schema, using the new _parse_payload() method to handle both inputs transparently.
🏷️ Updated Type Annotations
ninja_aio/models/serializers.py
ModelSerializerinner classes now accepttuple[str, Any]in addition totuple[str, Any, Any]for bothfieldsandcustomsattributes.SchemaModelConfig.customstype annotation updated toList[tuple[str, Any, Any] | tuple[str, Any]].
📝 Comprehensive Docstrings
ninja_aio/models/serializers.py
Added detailed NumPy-style docstrings with Parameters, Returns, and Raises sections to virtually all methods in BaseSerializer, ModelSerializer, and Serializer (30+ methods).
🎨 Documentation Overhaul
💎 Complete Site Redesign
All documentation pages updated with Material for MkDocs icons, grid cards, section dividers, and modern formatting:
- 🏠 Landing page — Hero section, CTA buttons, grid cards for features, tabbed code comparison, Schema Validators section, key concepts in card layout
- 📖 Tutorial pages — Hero banners with step indicators, learning objectives, prerequisites boxes, summary checklists
- 📑 API reference pages — Material icons on headings, section dividers, "See Also" replaced with grid cards
- 🎨 Custom CSS — New styles for hero sections, card grids, tutorial components, and release notes UI
- ⚙️ MkDocs theme — Added template overrides, announcement bar, emoji extension,
md_in_html, new navigation features
🖼️ README Redesign
README.md
- 🎯 Centered HTML layout: logo, title, subtitle, and badge row
- 📊 Features bullet list replaced with formatted table
- 🅰️🅱️ Quick Start restructured into "Option A" and "Option B" sections
- 🛡️ New "Schema Validators" section with examples and mapping table
- 🔄 "Lifecycle Hooks" bullet list replaced with table
- 🧹 Redundant sections removed, "Buy me a coffee" uses styled badge
🗂️ MkDocs Navigation Updates
mkdocs.yml
- ➕ Added
tutorial/serializer.md— "Alternative: Define Your Serializer" - ➕ Added
api/models/validators.md— "Validators" - ➕ Added
api/renderers/orjson_renderer.md— "Renderers"
🔄 Release Notes Page Redesign
main.py
Replaced table-based release notes layout with an interactive dropdown version selector and card-based display with human-readable date formatting.
🧪 Tests
tests/test_serializers.py,tests/test_app/models.py,tests/test_app/serializers.py
ValidatorsOnSerializersTestCase — 14 tests
🏗️ ModelSerializer validators:
| Test | Verifies |
|---|---|
test_model_serializer_field_validator_rejects_invalid |
❌ @field_validator on CreateSerializer rejects input below min length |
test_model_serializer_field_validator_accepts_valid |
✅ @field_validator on CreateSerializer accepts valid input |
test_model_serializer_update_validator_rejects_blank |
❌ @field_validator on UpdateSerializer rejects blank name |
test_model_serializer_update_validator_accepts_valid |
✅ @field_validator on UpdateSerializer accepts valid input |
test_model_serializer_read_model_validator |
✅ @model_validator on ReadSerializer is applied to output schema |
test_model_serializer_no_validators_returns_plain_schema |
✅ Serializers without validators still work normally |
🗺️ Meta-driven Serializer validators:
| Test | Verifies |
|---|---|
test_meta_serializer_field_validator_rejects_invalid |
❌ CreateValidators @field_validator rejects invalid input |
test_meta_serializer_field_validator_accepts_valid |
✅ CreateValidators @field_validator accepts valid input |
test_meta_serializer_update_validator_rejects_blank |
❌ UpdateValidators @field_validator rejects blank name |
test_meta_serializer_read_model_validator |
✅ ReadValidators @model_validator is applied to output schema |
🔧 Utility method tests:
| Test | Verifies |
|---|---|
test_collect_validators_returns_empty_for_none |
🔍 _collect_validators(None) returns {} |
test_collect_validators_returns_empty_for_no_validators |
🔍 _collect_validators returns {} for class without validators |
test_apply_validators_returns_none_for_none_schema |
🔍 _apply_validators(None, ...) returns None |
test_apply_validators_returns_schema_for_empty_validators |
🔍 _apply_validators(schema, {}) returns original schema |
📦 New test fixtures:
| File | Addition |
|---|---|
tests/test_app/models.py |
TestModelWithValidators — model with validators on CreateSerializer, UpdateSerializer, ReadSerializer |
tests/test_app/serializers.py |
TestModelWithValidatorsMetaSerializer — serializer with CreateValidators, UpdateValidators, ReadValidators |
📁 New Files
| File | Description |
|---|---|
CLAUDE.md |
📋 Project instructions: overview, structure, tests, code style, architecture notes |
CHANGELOG.md |
📝 Latest release notes |
🎯 Summary
This release introduces Pydantic validators on serializers, allowing @field_validator and @model_validator to be declared directly on serializer configuration classes. The framework automatically collects and applies these validators to generated schemas. Additionally, the entire documentation site has been redesigned with Material for MkDocs components.
🌟 Key benefits:
- 🛡️ Schema-level validation — Enforce input constraints beyond Django model fields, running before data touches the database
- 🔀 Per-operation validation — Apply different validation rules per CRUD operation (create vs. update vs. read)
- 🏗️ Both serializer patterns — Works with
ModelSerializer(inner classes) andSerializer({Type}Validatorsclasses) - ♻️ Backwards compatible — Existing serializers without validators continue to work unchanged
- 🎨 Documentation redesign — Modern Material for MkDocs layout with grid cards, hero sections, and interactive release notes
v2.17.0
Jan 28, 2026Release Notes
[v2.17.0] - 2026-01-28
✨ New Features
- Inline Custom Fields in
fieldsList [ninja_aio/models/serializers.py]: - Custom fields can now be defined directly in the
fieldslist as tuples, providing a more concise syntax. - Supports both 2-tuple
(name, type)for required fields and 3-tuple(name, type, default)for optional fields. - Works with both
ModelSerializer(inner classes) andSerializer(Meta-driven) approaches. - Applies to all serializer types:
CreateSerializer,ReadSerializer,DetailSerializer,UpdateSerializer, andSchemaModelConfig.
Usage example (ModelSerializer):
```python
from ninja_aio.models import ModelSerializer
class Article(ModelSerializer):
title = models.CharField(max_length=200)
content = models.TextField()
class ReadSerializer:
fields = [
"id",
"title",
("word_count", int, 0), # 3-tuple: optional with default
("is_featured", bool), # 2-tuple: required field
]
```
Usage example (Serializer):
```python
from ninja_aio.models import serializers
class ArticleSerializer(serializers.Serializer):
class Meta:
model = Article
schema_out = serializers.SchemaModelConfig(
fields=["id", "title", ("reading_time", int, 0)]
)
```
- New
get_inline_customs()Helper Method [ninja_aio/models/serializers.py]: - Added
BaseSerializer.get_inline_customs(s_type)method to extract and normalize inline custom tuples from thefieldslist. - Returns a list of normalized 3-tuples
(name, type, default), converting 2-tuples by adding...(Ellipsis) as the default.
🔧 Improvements
- Refactored
get_fields()Method [ninja_aio/models/serializers.py]: get_fields()now returns only string field names, excluding inline custom tuples.-
Clearer separation of concerns between model fields and custom fields.
-
Improved
get_related_schema_data()Method [ninja_aio/models/serializers.py]: - Fixed handling of custom fields that don't exist as model attributes.
-
Custom fields (both explicit and inline) are now always included in related schemas since they are computed/synthetic.
-
Updated
SchemaModelConfigType Annotations [ninja_aio/models/serializers.py]: - The
fieldsattribute now acceptsList[str | tuple[str, Any, Any] | tuple[str, Any]]to support inline customs. -
Updated docstring to document the new tuple formats.
-
Cleaner Schema Generation [
ninja_aio/models/serializers.py]: get_schema_out_data()and_generate_model_schema()now use the newget_inline_customs()helper, reducing code duplication.
🧪 Tests
- New Inline Customs Test Cases [
tests/test_serializers.py]: -
Added
InlineCustomsSerializerTestCasetest class with 11 tests for Meta-driven Serializer:test_serializer_read_schema_with_inline_customs_3_tuple: Verifies 3-tuple inline customs work in read schema.test_serializer_read_schema_with_inline_customs_2_tuple: Verifies 2-tuple inline customs work in read schema.test_serializer_create_schema_with_inline_customs: Verifies inline customs in create schema.test_serializer_update_schema_with_inline_customs: Verifies inline customs in update schema.test_serializer_inline_customs_combined_with_explicit_customs: Verifies inline and explicit customs coexist.test_serializer_get_fields_excludes_inline_customs: Verifiesget_fields()returns only strings.test_serializer_get_inline_customs_returns_only_tuples: Verifiesget_inline_customs()returns normalized tuples.test_serializer_detail_schema_with_inline_customs: Verifies inline customs in detail schema.test_serializer_related_schema_with_inline_customs: Verifies inline customs in related schema.test_inline_customs_only_schema: Verifies schema with only inline customs (no regular fields).
-
Added
InlineCustomsModelSerializerTestCasetest class with 4 tests for ModelSerializer:test_model_serializer_read_schema_with_inline_customs: Verifies inline customs in ReadSerializer.test_model_serializer_create_schema_with_inline_customs: Verifies inline customs in CreateSerializer.test_model_serializer_get_inline_customs: Verifiesget_inline_customs()for ModelSerializer.test_model_serializer_get_fields_excludes_inline_customs: Verifiesget_fields()excludes inline customs.
-
New Test Model [
tests/test_app/models.py]: - Added
TestModelSerializerInlineCustomsmodel with inline customs in bothReadSerializerandCreateSerializer.
📚 Documentation
- Updated Serializer Documentation [
docs/api/models/serializers.md]: - Added new "Inline Custom Fields" section with usage examples.
- Updated
SchemaModelConfigfields description to mention inline custom tuples. -
Added explanation of 2-tuple and 3-tuple formats.
-
Updated ModelSerializer Documentation [
docs/api/models/model_serializer.md]: - Updated all serializer attribute tables to show
list[str | tuple]type forfields. - Added "Inline Custom Fields" subsection in CreateSerializer with usage example.
- Updated ReadSerializer, DetailSerializer, and UpdateSerializer tables.
📋 Summary
This minor release introduces inline custom field support, allowing custom/computed fields to be defined directly in the fields list as tuples. This provides a more concise syntax for simple custom fields while maintaining full backwards compatibility with the separate customs list approach.
Key Benefits
- Concise syntax: Define simple custom fields inline without a separate
customslist - Flexibility: Mix regular fields and custom tuples in the same list
- Backwards compatible: Existing code using
customslist continues to work unchanged
Files Changed
| File | Changes |
|---|---|
ninja_aio/models/serializers.py |
Added get_inline_customs() method, updated get_fields(), get_schema_out_data(), _generate_model_schema(), get_related_schema_data(), and SchemaModelConfig |
tests/test_serializers.py |
Added 15 new tests across 2 test classes |
tests/test_app/models.py |
Added TestModelSerializerInlineCustoms test model |
docs/api/models/serializers.md |
Added inline custom fields documentation with examples |
docs/api/models/model_serializer.md |
Updated all serializer attribute tables and added inline customs section |
v2.16.2
Jan 27, 2026Release Notes
[v2.16.2] - 2026-01-27
🐛 Bug Fixes
- Fixed Schema Generation with Only Custom Fields [
ninja_aio/models/serializers.py]: - Fixed an issue in
_generate_model_schema()where defining onlycustomsand/oroptionals(without explicitfieldsorexcludes) would incorrectly include all model fields in the generated schema. - When only custom fields are defined, the schema now correctly excludes all concrete model fields, returning a schema with only the specified custom fields.
- This fix applies to both
Serializer(Meta-driven) andModelSerializercreate/update schema generation.
Before (broken behavior):
```python
class MySerializer(Serializer):
class Meta:
model = MyModel
schema_in = SchemaModelConfig(
customs=[("custom_input", str, ...)]
)
# Generated schema incorrectly included ALL model fields + custom_input
```
After (fixed behavior):
```python
class MySerializer(Serializer):
class Meta:
model = MyModel
schema_in = SchemaModelConfig(
customs=[("custom_input", str, ...)]
)
# Generated schema now correctly includes ONLY custom_input
```
🔧 Improvements
- Union Type Support in SchemaModelConfig [
ninja_aio/models/serializers.py]: - Updated
optionalsandcustomsfield type hints inSchemaModelConfigto acceptAnyinstead oftype. - This allows using Union types and other complex type annotations in schema configurations.
Usage example:
```python
from typing import Union
schema_in = SchemaModelConfig(
optionals=[("status", str | None)],
customs=[
("data", Union[str, int], None),
("items", list[int], []),
],
)
```
🧪 Tests
- New Custom Fields Schema Tests [
tests/test_serializers.py]: - Added
CustomsOnlySchemaTestCasetest class with 7 new tests:test_serializer_create_schema_with_only_customs: Verifies create schema with only customs excludes model fields.test_serializer_update_schema_with_only_customs: Verifies update schema with only customs excludes model fields.test_serializer_create_schema_with_customs_and_optionals: Verifies customs + optionals includes only those fields.test_serializer_with_fields_still_works: Confirms explicit fields behavior is preserved.test_serializer_with_only_excludes_and_customs: Documents behavior when excludes defined without fields.test_serializer_empty_schema_returns_none: Verifies empty schema returns None.test_serializer_multiple_customs_no_model_fields: Verifies multiple customs work without model fields.
📋 Summary
This patch release fixes a bug where schemas defined with only custom fields would incorrectly include all model fields, and adds support for Union types in SchemaModelConfig field definitions.
Files Changed
| File | Changes |
|---|---|
ninja_aio/models/serializers.py |
Fixed _generate_model_schema() to exclude all model fields when only customs are defined; updated SchemaModelConfig type hints to allow Union types |
tests/test_serializers.py |
Added 7 new tests in CustomsOnlySchemaTestCase |
ninja_aio/__init__.py |
Bumped version to 2.16.2 |
[v2.16.0] - 2026-01-26
✨ New Features
- Custom Decorators for M2M Relation Endpoints [
ninja_aio/schemas/helpers.py,ninja_aio/helpers/api.py]: - Added
get_decoratorsfield toM2MRelationSchemafor applying custom decorators to GET (list related objects) endpoints. - Added
post_decoratorsfield toM2MRelationSchemafor applying custom decorators to POST (add/remove) endpoints. - Decorators are unpacked and applied via
decorate_view()alongside existing decorators likeunique_viewandpaginate. - Enables use cases such as rate limiting, caching, custom authentication, logging, or any other decorator-based middleware on M2M endpoints.
Usage example:
```python
from ninja_aio.schemas import M2MRelationSchema
M2MRelationSchema(
model=RelatedModel,
related_name="related_items",
get_decorators=[cache_decorator, log_decorator],
post_decorators=[rate_limit_decorator],
)
```
🔧 Improvements
- Refactored Manage Relation View Registration [
ninja_aio/helpers/api.py]: - Updated
_register_manage_relation_view()to usedecorate_view()wrapper instead of direct@unique_viewdecorator. - Ensures consistent decorator application pattern between GET and POST endpoints.
-
Allows decorator spreading via
*decoratorsfor extensibility. -
Improved Type Hints [
ninja_aio/schemas/helpers.py]: - Added
Callableimport from typing module. - Updated
get_decoratorsandpost_decoratorstype hints toOptional[List[Callable]]for better IDE support and type checking.
🧪 Tests
- New Decorator Integration Tests [
tests/helpers/test_many_to_many_api.py]: - Added
M2MRelationSchemaDecoratorsTestCasetest class with integration tests:test_get_decorator_is_applied: Verifies GET decorators are invoked on list endpoint calls.test_post_decorator_is_applied: Verifies POST decorators are invoked on add/remove endpoint calls.test_decorators_independent: Confirms GET and POST decorators operate independently.
-
Added
TestM2MWithDecoratorsViewSettest viewset demonstrating decorator usage. -
New Decorator Schema Validation Tests [
tests/helpers/test_many_to_many_api.py]: - Added
M2MRelationSchemaDecoratorsFieldTestCasetest class with schema field tests:test_decorators_default_to_empty_list: Validates default empty list behavior.test_decorators_accept_list_of_callables: Validates callable list acceptance.test_decorators_can_be_none: Validates explicitNoneassignment.
📚 Documentation
- Updated APIViewSet Documentation [
docs/api/views/api_view_set.md]: - Added
get_decoratorsandpost_decoratorsto M2MRelationSchema attributes list. - Added comprehensive example showing custom decorator usage with M2M relations (cache and rate limiting patterns).
-
Added note explaining decorator application order and interaction with built-in decorators.
-
Updated Decorators Documentation [
docs/api/views/decorators.md]: - Added new "M2MRelationSchema decorators" section.
-
Included usage example and cross-reference to APIViewSet M2M Relations documentation.
-
Split Quick Start into Two Guides:
- [
docs/getting_started/quick_start.md]: Dedicated toModelSerializerapproach with embedded serializer configuration. - [
docs/getting_started/quick_start_serializer.md]: New guide forSerializerapproach with plain Django models, including examples for relationships, query optimization, and lifecycle hooks.
📋 Summary
This minor release introduces custom decorator support for Many-to-Many relation endpoints. Users can now apply custom decorators independently to GET and POST M2M endpoints via the new get_decorators and post_decorators fields in M2MRelationSchema. This enables flexible middleware patterns such as caching, rate limiting, and custom logging on relation endpoints.
Files Changed
| File | Changes |
|---|---|
ninja_aio/schemas/helpers.py |
Added get_decorators and post_decorators fields with Callable type hints |
ninja_aio/helpers/api.py |
Updated view registration to accept and apply custom decorators |
tests/helpers/test_many_to_many_api.py |
Added 6 new tests across 2 test classes |
docs/api/views/api_view_set.md |
Documented M2M decorator fields with usage examples |
docs/api/views/decorators.md |
Added M2MRelationSchema decorators section |
docs/getting_started/quick_start.md |
Dedicated to ModelSerializer approach |
docs/getting_started/quick_start_serializer.md |
New guide for Serializer approach with plain Django models |
mkdocs.yml |
Updated navigation with two Quick Start guides |
v2.16.1
Jan 27, 2026Release Notes
[v2.16.1] - 2026-01-27
🐛 Bug Fixes
- Fixed Schema Generation with Only Custom Fields [
ninja_aio/models/serializers.py]: - Fixed an issue in
_generate_model_schema()where defining onlycustomsand/oroptionals(without explicitfieldsorexcludes) would incorrectly include all model fields in the generated schema. - When only custom fields are defined, the schema now correctly excludes all concrete model fields, returning a schema with only the specified custom fields.
- This fix applies to both
Serializer(Meta-driven) andModelSerializercreate/update schema generation.
Before (broken behavior):
```python
class MySerializer(Serializer):
class Meta:
model = MyModel
schema_in = SchemaModelConfig(
customs=[("custom_input", str, ...)]
)
# Generated schema incorrectly included ALL model fields + custom_input
```
After (fixed behavior):
```python
class MySerializer(Serializer):
class Meta:
model = MyModel
schema_in = SchemaModelConfig(
customs=[("custom_input", str, ...)]
)
# Generated schema now correctly includes ONLY custom_input
```
🧪 Tests
- New Custom Fields Schema Tests [
tests/test_serializers.py]: - Added
CustomsOnlySchemaTestCasetest class with 7 new tests:test_serializer_create_schema_with_only_customs: Verifies create schema with only customs excludes model fields.test_serializer_update_schema_with_only_customs: Verifies update schema with only customs excludes model fields.test_serializer_create_schema_with_customs_and_optionals: Verifies customs + optionals includes only those fields.test_serializer_with_fields_still_works: Confirms explicit fields behavior is preserved.test_serializer_with_only_excludes_and_customs: Documents behavior when excludes defined without fields.test_serializer_empty_schema_returns_none: Verifies empty schema returns None.test_serializer_multiple_customs_no_model_fields: Verifies multiple customs work without model fields.
📋 Summary
This patch release fixes a bug where schemas defined with only custom fields would incorrectly include all model fields. The fix ensures that when only customs are specified (without fields or excludes), the generated schema contains only the custom fields as intended.
Files Changed
| File | Changes |
|---|---|
ninja_aio/models/serializers.py |
Fixed _generate_model_schema() to exclude all model fields when only customs are defined |
tests/test_serializers.py |
Added 7 new tests in CustomsOnlySchemaTestCase |
ninja_aio/__init__.py |
Bumped version to 2.16.1 |
[v2.16.0] - 2026-01-26
✨ New Features
- Custom Decorators for M2M Relation Endpoints [
ninja_aio/schemas/helpers.py,ninja_aio/helpers/api.py]: - Added
get_decoratorsfield toM2MRelationSchemafor applying custom decorators to GET (list related objects) endpoints. - Added
post_decoratorsfield toM2MRelationSchemafor applying custom decorators to POST (add/remove) endpoints. - Decorators are unpacked and applied via
decorate_view()alongside existing decorators likeunique_viewandpaginate. - Enables use cases such as rate limiting, caching, custom authentication, logging, or any other decorator-based middleware on M2M endpoints.
Usage example:
```python
from ninja_aio.schemas import M2MRelationSchema
M2MRelationSchema(
model=RelatedModel,
related_name="related_items",
get_decorators=[cache_decorator, log_decorator],
post_decorators=[rate_limit_decorator],
)
```
🔧 Improvements
- Refactored Manage Relation View Registration [
ninja_aio/helpers/api.py]: - Updated
_register_manage_relation_view()to usedecorate_view()wrapper instead of direct@unique_viewdecorator. - Ensures consistent decorator application pattern between GET and POST endpoints.
-
Allows decorator spreading via
*decoratorsfor extensibility. -
Improved Type Hints [
ninja_aio/schemas/helpers.py]: - Added
Callableimport from typing module. - Updated
get_decoratorsandpost_decoratorstype hints toOptional[List[Callable]]for better IDE support and type checking.
🧪 Tests
- New Decorator Integration Tests [
tests/helpers/test_many_to_many_api.py]: - Added
M2MRelationSchemaDecoratorsTestCasetest class with integration tests:test_get_decorator_is_applied: Verifies GET decorators are invoked on list endpoint calls.test_post_decorator_is_applied: Verifies POST decorators are invoked on add/remove endpoint calls.test_decorators_independent: Confirms GET and POST decorators operate independently.
-
Added
TestM2MWithDecoratorsViewSettest viewset demonstrating decorator usage. -
New Decorator Schema Validation Tests [
tests/helpers/test_many_to_many_api.py]: - Added
M2MRelationSchemaDecoratorsFieldTestCasetest class with schema field tests:test_decorators_default_to_empty_list: Validates default empty list behavior.test_decorators_accept_list_of_callables: Validates callable list acceptance.test_decorators_can_be_none: Validates explicitNoneassignment.
📚 Documentation
- Updated APIViewSet Documentation [
docs/api/views/api_view_set.md]: - Added
get_decoratorsandpost_decoratorsto M2MRelationSchema attributes list. - Added comprehensive example showing custom decorator usage with M2M relations (cache and rate limiting patterns).
-
Added note explaining decorator application order and interaction with built-in decorators.
-
Updated Decorators Documentation [
docs/api/views/decorators.md]: - Added new "M2MRelationSchema decorators" section.
-
Included usage example and cross-reference to APIViewSet M2M Relations documentation.
-
Split Quick Start into Two Guides:
- [
docs/getting_started/quick_start.md]: Dedicated toModelSerializerapproach with embedded serializer configuration. - [
docs/getting_started/quick_start_serializer.md]: New guide forSerializerapproach with plain Django models, including examples for relationships, query optimization, and lifecycle hooks.
📋 Summary
This minor release introduces custom decorator support for Many-to-Many relation endpoints. Users can now apply custom decorators independently to GET and POST M2M endpoints via the new get_decorators and post_decorators fields in M2MRelationSchema. This enables flexible middleware patterns such as caching, rate limiting, and custom logging on relation endpoints.
Files Changed
| File | Changes |
|---|---|
ninja_aio/schemas/helpers.py |
Added get_decorators and post_decorators fields with Callable type hints |
ninja_aio/helpers/api.py |
Updated view registration to accept and apply custom decorators |
tests/helpers/test_many_to_many_api.py |
Added 6 new tests across 2 test classes |
docs/api/views/api_view_set.md |
Documented M2M decorator fields with usage examples |
docs/api/views/decorators.md |
Added M2MRelationSchema decorators section |
docs/getting_started/quick_start.md |
Dedicated to ModelSerializer approach |
docs/getting_started/quick_start_serializer.md |
New guide for Serializer approach with plain Django models |
mkdocs.yml |
Updated navigation with two Quick Start guides |
v2.15.0
Jan 22, 2026Release Notes
[v2.15.0] - 2026-01-22
New Features
- Dynamic PK Type Detection for
relations_as_id[ninja_aio/models/serializers.py]: - The
PkFromModeltype now automatically detects and uses the related model's primary key type. - Supports
int(default),UUID,str(CharField), and any other Django primary key type. - Schema generation now correctly annotates relation fields with the appropriate PK type.
Improvements
PkFromModelSubscriptable Type [ninja_aio/models/serializers.py]:- New
PkFromModel[type]syntax allows explicit PK type specification. - Examples:
PkFromModel[int],PkFromModel[UUID],PkFromModel[str]. - Falls back to
intwhen used without subscription (backwards compatible). - Uses
BeforeValidatorto extractpkattribute during Pydantic serialization.
Tests
- Comprehensive Test Coverage for Different PK Types [
tests/test_serializers.py]: - Added
RelationsAsIdUUIDModelSerializerTestCase(6 tests) - Schema generation tests for UUID PKs - Added
RelationsAsIdUUIDIntegrationTestCase(7 tests) - Integration tests with UUID PK data - Added
RelationsAsIdStringPKModelSerializerTestCase(6 tests) - Schema generation tests for string PKs - Added
RelationsAsIdStringPKIntegrationTestCase(7 tests) - Integration tests with string PK data -
Coverage includes all relation types: Forward FK, Reverse FK, Forward O2O, Reverse O2O, Forward M2M, Reverse M2M
-
New Test Models with Different PK Types [
tests/test_app/models.py]: - UUID PK models:
AuthorUUID,BookUUID,ProfileUUID,UserUUID,TagUUID,ArticleUUID - String PK models:
AuthorStringPK,BookStringPK,ProfileStringPK,UserStringPK,TagStringPK,ArticleStringPK
Documentation
- ModelSerializer Documentation [
docs/api/models/model_serializer.md]: - Updated
relations_as_idtable to showPK_TYPEinstead of hardcodedint. - Added note explaining automatic PK type detection.
-
Added UUID primary key example with code and JSON output.
-
Serializer Documentation [
docs/api/models/serializers.md]: - Updated
relations_as_idtable to showPK_TYPEinstead of hardcodedint. - Added note explaining automatic PK type detection.
- Added UUID primary key example with code and JSON output.
Usage Example
UUID Primary Key with relations_as_id
import uuid
from django.db import models
from ninja_aio.models import ModelSerializer
class Author(ModelSerializer):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=200)
class ReadSerializer:
fields = ["id", "name", "books"]
relations_as_id = ["books"] # Reverse FK as list of UUIDs
class Book(ModelSerializer):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
class ReadSerializer:
fields = ["id", "title", "author"]
relations_as_id = ["author"] # Forward FK as UUID
Output (Author with UUID PK):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "J.K. Rowling",
"books": [
"6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"6ba7b811-9dad-11d1-80b4-00c04fd430c8"
]
}
Output (Book with UUID PK):
{
"id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"title": "Harry Potter",
"author": "550e8400-e29b-41d4-a716-446655440000"
}
String Primary Key with relations_as_id
from django.db import models
from ninja_aio.models import ModelSerializer
class Author(ModelSerializer):
id = models.CharField(primary_key=True, max_length=50)
name = models.CharField(max_length=200)
class ReadSerializer:
fields = ["id", "name", "books"]
relations_as_id = ["books"]
class Book(ModelSerializer):
id = models.CharField(primary_key=True, max_length=50)
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
class ReadSerializer:
fields = ["id", "title", "author"]
relations_as_id = ["author"]
Output (Author with String PK):
{
"id": "author-001",
"name": "J.K. Rowling",
"books": ["book-001", "book-002", "book-003"]
}
Output (Book with String PK):
{
"id": "book-001",
"title": "Harry Potter",
"author": "author-001"
}
v2.14.0
Jan 22, 2026Release Notes
[v2.14.0] - 2026-01-22
🐛 Fixed
- Forward Relations in
relations_as_idNow Work Correctly [ninja_aio/models/serializers.py]: - Fixed issue where forward FK and O2O relations listed in
relations_as_idwere serialized asnullinstead of the related object's ID. - Root Cause: Previously used
validation_aliaswhich only affects input parsing, not output serialization. - Solution: Now uses
PkFromModelwithBeforeValidatorto extract the primary key during serialization, consistent with reverse relations. - Affects: Forward ForeignKey, Forward OneToOneField.
🔧 Improvements
- Optimized
relations_as_idProcessing [ninja_aio/models/serializers.py]: _get_relations_as_id()is now called once inget_schema_out_data()and passed through to child methods, eliminating redundant method calls during schema generation.-
_build_schema_reverse_rel(),_build_schema_forward_rel(), and_process_field()now acceptrelations_as_idas a parameter instead of fetching it independently. -
Enhanced Method Documentation [
ninja_aio/models/serializers.py]: - Added comprehensive docstrings with parameter descriptions to:
_build_schema_reverse_rel()- Documents descriptor types and return values_build_schema_forward_rel()- Documents forward relation handling logic_process_field()- Documents field classification processget_schema_out_data()- Documents schema component collection
🧪 Tests
- Comprehensive Test Coverage for
relations_as_id[tests/test_serializers.py]: - Added
RelationsAsIdModelSerializerTestCase(6 tests) - Schema generation tests for ModelSerializer - Added
RelationsAsIdSerializerTestCase(6 tests) - Schema generation tests for Meta-driven Serializer - Added
RelationsAsIdIntegrationTestCase(7 tests) - Integration tests with actual data serialization - Coverage includes all relation types: Forward FK, Reverse FK, Forward O2O, Reverse O2O, Forward M2M, Reverse M2M
-
Tests null value handling for nullable forward relations
-
New Test Models [
tests/test_app/models.py]: AuthorAsId,BookAsId- FK relation testingProfileAsId,UserAsId- O2O relation testing-
TagAsId,ArticleAsId- M2M relation testing -
New Test Serializers [
tests/test_app/serializers.py]: BookAsIdMetaSerializer,AuthorAsIdMetaSerializer- FK with Meta-driven SerializerUserAsIdMetaSerializer,ProfileAsIdMetaSerializer- O2O with Meta-driven SerializerArticleAsIdMetaSerializer,TagAsIdMetaSerializer- M2M with Meta-driven Serializer
📖 Documentation
- ModelSerializer Documentation [
docs/api/models/model_serializer.md]: - Added
relations_as_idattribute toReadSerializerattributes table. -
Added new "Relations as ID" section with:
- Use cases (payload size, circular serialization, performance, API design)
- Supported relations table with output types
- FK, O2O, and M2M examples with JSON output
- Query optimization note for
select_related/prefetch_related
-
Serializer Documentation [
docs/api/models/serializers.md]: - Added
relations_as_idto Meta configuration options. - Added new "Relations as ID" section with:
- Complete examples for FK, O2O, and M2M relations
- Guide for combining
relations_as_idwithrelations_serializers - Query optimization recommendations
💡 Usage Example
ModelSerializer with relations_as_id
from ninja_aio.models import ModelSerializer
from django.db import models
class Author(ModelSerializer):
name = models.CharField(max_length=200)
class ReadSerializer:
fields = ["id", "name", "books"]
relations_as_id = ["books"] # Reverse FK as list of IDs
class Book(ModelSerializer):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
class ReadSerializer:
fields = ["id", "title", "author"]
relations_as_id = ["author"] # Forward FK as ID
Output (Author):
{
"id": 1,
"name": "J.K. Rowling",
"books": [1, 2, 3]
}
Output (Book):
{
"id": 1,
"title": "Harry Potter",
"author": 1
}
Meta-driven Serializer with relations_as_id
from ninja_aio.models import serializers
class ArticleSerializer(serializers.Serializer):
class Meta:
model = Article
schema_out = serializers.SchemaModelConfig(
fields=["id", "title", "author", "tags"]
)
relations_serializers = {
"author": AuthorSerializer, # Nested object
}
relations_as_id = ["tags"] # M2M as list of IDs
class QuerySet:
read = ModelQuerySetSchema(
select_related=["author"],
prefetch_related=["tags"],
)
Output:
{
"id": 1,
"title": "Getting Started with Django",
"author": {"id": 1, "name": "John Doe"},
"tags": [1, 2, 5]
}
v2.13.0
Jan 21, 2026Release Notes
[v2.13.0] - 2026-01-21
✨ Added
verbose_name_pluralAttribute for M2MRelationSchema [ninja_aio/schemas/helpers.py]:- New optional
verbose_name_pluralfield allows customizing the human-readable plural name for M2M relation endpoints. - When provided, used in endpoint summaries and descriptions (e.g.,
"Get Article Tags","Add or Remove Article Tags"). - Falls back to
model._meta.verbose_name_plural.capitalize()when not specified.
🔧 Improvements
- Refactored M2M Endpoint Summary Generation [
ninja_aio/helpers/api.py]: _register_get_relation_view()now acceptsverbose_name_pluralparameter instead of computing it internally._register_manage_relation_view()simplified by removingrel_utilparameter; now receivesverbose_name_pluraldirectly._build_views()centralizes verbose name resolution with fallback logic.-
Cleaner separation of concerns: verbose name is resolved once and passed to both GET and POST registration methods.
-
Simplified Warning Logic for Missing Relation Serializers [
ninja_aio/models/serializers.py]: _warn_missing_relation_serializer()now uses simpler boolean logic.- Warnings emit when model is not a
ModelSerializerandNINJA_AIO_RAISE_SERIALIZATION_WARNINGSisTrue(default). - Removed dependency on
NINJA_AIO_TESTINGsetting for warning control.
📖 Documentation
- Updated M2MRelationSchema Documentation [
docs/api/views/api_view_set.md]: - Added
verbose_name_pluralto the list of M2MRelationSchema attributes. - Added usage example demonstrating custom verbose names for M2M endpoints.
💡 Usage Example
Custom Verbose Names for M2M Endpoints
from ninja_aio.views import APIViewSet
from ninja_aio.schemas.helpers import M2MRelationSchema
@api.viewset(model=Article)
class ArticleViewSet(APIViewSet):
m2m_relations = [
M2MRelationSchema(
model=Tag,
related_name="tags",
verbose_name_plural="Article Tags", # Custom name
add=True,
remove=True,
get=True,
),
M2MRelationSchema(
model=Category,
related_name="categories",
# Uses default: "Categories" (from model._meta.verbose_name_plural)
get=True,
),
]
Generated Endpoint Summaries:
- GET /articles/{pk}/tags → Summary: "Get Article Tags"
- POST /articles/{pk}/tags/ → Summary: "Add or Remove Article Tags"
- GET /articles/{pk}/categories → Summary: "Get Categories"
v2.12.3
Jan 21, 2026Release Notes
[v2.12.3] - 2026-01-21
🐛 Fixed
- Warning Logic for Missing Relation Serializers [
ninja_aio/models/serializers.py]: - Fixed boolean logic in
_warn_missing_relation_serializermethod.
v2.12.2
Jan 21, 2026Release Notes
[v2.12.2] - 2026-01-21
🐛 Fixed
- Warning Logic for Missing Relation Serializers [
ninja_aio/models/serializers.py]: - Fixed boolean logic in
_warn_missing_relation_serializermethod. - Warnings now correctly emit when: (not ModelSerializer AND not testing) OR force warnings enabled.
- Previously, warnings were incorrectly suppressed for non-ModelSerializer relations even when
NINJA_AIO_RAISE_SERIALIZATION_WARNINGS=True.
v2.12.1
Jan 21, 2026Release Notes
[v2.12.1] - 2026-01-21
🐛 Fixed
- QueryUtil DETAIL Scope Fallback [
ninja_aio/helpers/query.py]: detail_confignow correctly falls back toread_configforselect_relatedandprefetch_relatedwhen not explicitly configured.- Ensures consistent queryset optimization behavior between READ and DETAIL scopes.
🔧 Improvements
- Refactored Fallback Logic [
ninja_aio/helpers/query.py]: - Moved DETAIL→READ fallback from
apply_queryset_optimizations()to__init__(). - Fallback is now resolved once at construction time, improving clarity and preventing runtime mutation.
- Uses
.copy()to ensuredetail_configlists are independent fromread_config.
🧪 Tests
- New Test Cases for QueryUtil DETAIL Fallback [
tests/test_query_util.py]: test_detail_scope_fallback_to_read_select_related: Verifies DETAIL scope uses READ'sselect_relatedwhen not configured.test_detail_scope_fallback_to_read_prefetch_related: Verifies DETAIL scope uses READ'sprefetch_relatedwhen not configured.test_detail_config_initialized_with_read_fallback: Confirms fallback is applied during initialization.test_detail_config_independent_copy: Ensuresdetail_configlists are copies, not references (prevents mutation bugs).
v2.12.0
Jan 21, 2026Release Notes
[v2.12.0] - 2026-01-21
✨ Added
- Per-Field-Type Detail Fallback for ModelSerializer [
ninja_aio/models/serializers.py]: DetailSerializernow falls back toReadSerializerfor each field type (fields,customs,optionals,excludes) independently when not explicitly configured.-
Allows partial overrides: define only
DetailSerializer.fieldswhile inheritingcustoms,optionals, andexcludesfromReadSerializer. -
Schema-Level Detail Fallback for Serializer [
ninja_aio/models/serializers.py]: - When
schema_detailis not defined,Serializernow correctly falls back toschema_outfor all field configurations. -
Enables seamless list/detail endpoint differentiation without duplicating configuration.
-
New Setting:
NINJA_AIO_RAISE_SERIALIZATION_WARNINGS[ninja_aio/models/serializers.py]: - New Django setting to control serialization warning behavior during testing.
- When
True(withNINJA_AIO_TESTING=True), warnings for missing relation serializers are raised instead of suppressed.
🔧 Improvements
- Refactored Fallback Logic [
ninja_aio/models/serializers.py]: - Moved detail→read fallback from
BaseSerializer.get_fields()to_get_fields()in bothModelSerializerandSerializer. ModelSerializer._get_fields(): Falls back per-field-type (ifDetailSerializer.customsis empty, usesReadSerializer.customs).-
Serializer._get_fields(): Falls back at schema level (ifschema_detailisNone, usesschema_out). -
Warning Control Enhancement [
ninja_aio/models/serializers.py]: - Updated
_warn_missing_relation_serializer()to respect bothNINJA_AIO_TESTINGand newNINJA_AIO_RAISE_SERIALIZATION_WARNINGSsettings.
🐛 Fixed
- Serializer Detail Fallback Typo [
ninja_aio/models/serializers.py]: - Fixed
Serializer._get_fields()where detail fallback was incorrectly referencing"read"instead of"out"schema key.
🧪 Tests
- New Test Cases for Serializer Detail Fallback [
tests/test_serializers.py]: test_detail_fallback_customs_from_read: Verifies customs inheritance whenschema_detailis not defined.test_detail_fallback_optionals_from_read: Verifies optionals inheritance.test_detail_fallback_excludes_from_read: Verifies excludes inheritance.-
test_detail_does_not_inherit_when_defined: Confirms no inheritance whenschema_detailis explicitly defined. -
New Test Cases for ModelSerializer Detail Fallback [
tests/test_serializers.py]: test_model_serializer_detail_fallback_fields: Verifies fields fallback toReadSerializer.test_model_serializer_detail_fallback_customs: Verifies customs fallback per-field-type.test_model_serializer_detail_fallback_optionals: Verifies optionals fallback per-field-type.test_model_serializer_detail_fallback_excludes: Verifies excludes fallback per-field-type.test_model_serializer_detail_inherits_per_field_type: Confirms per-field-type inheritance behavior.-
test_model_serializer_with_detail_generates_different_schemas: End-to-end schema generation test. -
New Test Models [
tests/test_app/models.py]: TestModelSerializerWithReadCustoms: Model with customs onReadSerializeronly.TestModelSerializerWithReadOptionals: Model with optionals onReadSerializeronly.TestModelSerializerWithReadExcludes: Model with excludes onReadSerializeronly.TestModelSerializerWithBothSerializers: Model with bothReadSerializerandDetailSerializerconfigured.
💡 Usage Example
ModelSerializer (Per-Field-Type Fallback)
from ninja_aio.models import ModelSerializer
from django.db import models
class Article(ModelSerializer):
title = models.CharField(max_length=200)
content = models.TextField()
author_notes = models.TextField(blank=True)
class ReadSerializer:
fields = ["id", "title"]
customs = [("word_count", int, lambda obj: len(obj.content.split()))]
class DetailSerializer:
# Only override fields - customs inherited from ReadSerializer
fields = ["id", "title", "content", "author_notes"]
Behavior:
- generate_read_s() → {"id", "title", "word_count"}
- generate_detail_s() → {"id", "title", "content", "author_notes", "word_count"} (customs inherited)
Serializer (Schema-Level Fallback)
from ninja_aio.models import serializers
class ArticleSerializer(serializers.Serializer):
class Meta:
model = Article
schema_out = serializers.SchemaModelConfig(
fields=["id", "title"],
customs=[("word_count", int, 0)],
)
# No schema_detail - falls back to schema_out entirely
Behavior:
- generate_read_s() → {"id", "title", "word_count"}
- generate_detail_s() → {"id", "title", "word_count"} (same as read)
v2.11.2
Jan 19, 2026v2.11.2
Fixed
- Fixed binary field serialization in
_bump_object_from_schema- removedmode="json"frommodel_dump()to prevent UTF-8 decode errors when retrieving binary data. Binary fields are now properly handled byORJSONRendererwhich converts them to base64.
v2.11.1
Jan 19, 2026Release Notes
[v2.11.1] - 2026-01-19
🐛 Bug Fixes
- ORJSONRenderer HttpResponse Passthrough [
ninja_aio/renders.py]: - Fixed
ORJSONRenderer.render()to detect and returnHttpResponseBaseinstances directly without JSON serialization. - Previously, returning an
HttpResponsewith a custom content type (e.g., PEM files, binary downloads) would fail because the renderer attempted to serialize it as JSON. - Now supports returning
HttpResponse,StreamingHttpResponse, and any otherHttpResponseBasesubclass.
📖 Documentation
- ORJSON Renderer Documentation [
docs/api/renderers/orjson_renderer.md]: - Reorganized documentation with proper section headings.
- Added new HttpResponse Passthrough section explaining the feature.
- Includes usage examples for
HttpResponseandStreamingHttpResponse. - Documents the correct pattern for returning custom responses with non-JSON content types.
🧪 Tests
- New Test Cases [
tests/core/test_renderer_parser.py]: test_renderer_http_response_passthrough: VerifiesHttpResponseobjects pass through unchanged with correct content and headers.test_renderer_streaming_http_response_passthrough: VerifiesStreamingHttpResponseobjects are also handled correctly.
💡 Usage Example
from django.http import HttpResponse, StreamingHttpResponse
# Return a PEM file
@api.get("/public-key")
def get_public_key(request):
return HttpResponse(
settings.JWT_PUBLIC_KEY.as_pem(),
content_type="application/x-pem-file",
status=200,
)
# Return a streaming response for large files
@api.get("/download")
def download_file(request):
return StreamingHttpResponse(
file_iterator(),
content_type="application/octet-stream",
)
Note: Set the
statusparameter on theHttpResponseitself. Do not use tuple returns likereturn 200, HttpResponse(...).
v2.11.0
Jan 19, 2026Release Notes
[v2.11.0] - 2026-01-19
✨ Added
- MatchCaseFilterViewSetMixin [
ninja_aio/views/mixins.py]: - New mixin for conditional filtering based on boolean query parameters.
- Maps boolean API parameters (
?is_active=true) to different Django ORM filter conditions forTrueandFalsecases. - Supports both
filter()(include) andexclude()operations via theincludeattribute. -
Automatically registers query params from
filters_match_casesconfiguration. -
New Filter Schemas [
ninja_aio/schemas/filters.py]: MatchCaseFilterSchema: Configures match-case filters withquery_paramandcasesattributes.MatchConditionFilterSchema: Defines individual filter conditions withquery_filter(dict) andinclude(bool).BooleanMatchFilterSchema: Groupstrueandfalsecase conditions.FilterSchema: New base class for filter schemas withfilter_typeandquery_paramattributes.
🔧 Improvements
- Unified Special Filter Detection [
ninja_aio/views/api.py]: - Added
APIViewSet._check_match_cases_filters(filter: str)helper method. -
Added
APIViewSet._is_special_filter(filter: str)method combining relation and match-case filter detection. -
Filter Mixin Skip Logic [
ninja_aio/views/mixins.py]: - Updated all filter mixins to use
_is_special_filter()instead of_check_relations_filters():IcontainsFilterViewSetMixinBooleanFilterViewSetMixinNumericFilterViewSetMixinDateFilterViewSetMixin
-
Ensures match-case filter params are not double-processed by type-based mixins.
-
RelationFilterSchema Refactoring [
ninja_aio/schemas/filters.py]: RelationFilterSchemanow extendsFilterSchemabase class.- Moved from
ninja_aio/schemas/api.pyto dedicatedninja_aio/schemas/filters.pymodule.
📖 Documentation
- Mixins Documentation [
docs/api/views/mixins.md]: - Added comprehensive documentation for
MatchCaseFilterViewSetMixin. - Includes usage examples for simple status filtering and complex multi-condition filtering.
- Documents all schema requirements and configuration options.
🧪 Tests
- New Test Cases [
tests/views/test_viewset.py]: MatchCaseFilterViewSetMixinTestCase: Tests include behavior withTrue/False/Nonevalues.MatchCaseFilterViewSetMixinExcludeTestCase: Tests exclude behavior wheninclude=False.-
Tests cover query params registration and
filters_match_cases_fieldsproperty. -
Test ViewSets [
tests/test_app/views.py]: TestModelSerializerMatchCaseFilterAPI: Testsis_approvedfilter with include/exclude logic.-
TestModelSerializerMatchCaseExcludeFilterAPI: Testshide_pendingfilter with inverse logic. -
Test Model Update [
tests/test_app/models.py]: - Added
statusfield toTestModelSerializerfor match-case filter testing.
💡 Usage Example
from ninja_aio.views.mixins import MatchCaseFilterViewSetMixin
from ninja_aio.views.api import APIViewSet
from ninja_aio.schemas import (
MatchCaseFilterSchema,
MatchConditionFilterSchema,
BooleanMatchFilterSchema,
)
class OrderViewSet(MatchCaseFilterViewSetMixin, APIViewSet):
model = models.Order
api = api
filters_match_cases = [
MatchCaseFilterSchema(
query_param="is_completed",
cases=BooleanMatchFilterSchema(
true=MatchConditionFilterSchema(
query_filter={"status": "completed"},
),
false=MatchConditionFilterSchema(
query_filter={"status": "completed"},
include=False, # excludes completed orders
),
),
),
]
API Behavior:
- GET /orders?is_completed=true → queryset.filter(status="completed")
- GET /orders?is_completed=false → queryset.exclude(status="completed")
v2.10.1
Jan 16, 2026Release Notes
[v2.10.1] - 2026-01-16
🐛 Fixed
- Filter Mixin Conflict Resolution [file:2]:
- Added
APIViewSet._check_relations_filters(filter: str)helper method to detect if a filter key belongs torelations_filters. -
Added
RelationFilterViewSetMixin.relations_filters_fieldsproperty that extracts allquery_paramnames from configuredrelations_filters. -
Filter Handler Skip Logic [file:2]:
- IcontainsFilterViewSetMixin: Now skips relation filter keys (
if isinstance(value, str) and not self._check_relations_filters(key)). - BooleanFilterViewSetMixin: Now skips relation filter keys when applying boolean filters.
- NumericFilterViewSetMixin: Now skips relation filter keys when applying numeric filters.
- DateFilterViewSetMixin: Now skips relation filter keys when applying date comparisons (
__lte, etc.).
Impact: Prevents double-processing of relation filters when combining RelationFilterViewSetMixin with other filter mixins. Relation filters are handled exclusively by RelationFilterViewSetMixin.query_params_handler, avoiding conflicts like:
- String relation params (?author_id=5) being misinterpreted as icontains filters.
- Numeric relation params being applied twice.
- Boolean/date relation params triggering incorrect transformations.
🔧 Internal Changes
- Mixin Inheritance Chain:
- Filter mixins (
IcontainsFilterViewSetMixin, etc.) now respectRelationFilterViewSetMixinconfiguration via shared_check_relations_filters()method. -
Ensures proper layering: base filters → relation filters (exclusive handling).
-
Query Params Handler Flow:
v2.10.0
Jan 16, 2026Release Notes
[v2.10.0] - 2026-01-16
✨ Added
- Relation-Based Filtering Mixin:
- New
RelationFilterViewSetMixinfor filtering by related model fields via query parameters. - New
RelationFilterSchemafor declarative mapping ofquery_param→ Django ORMquery_filterwith typedfilter_typetuples. -
Automatic registration of
relations_filtersentries intoquery_paramson subclasses. -
Schema & Import Enhancements:
- Exported
RelationFilterSchemafromninja_aio.schemasand added to__all__. - Added
RelationFilterSchemaimport toninja_aio.views.mixinsand example usages in docs and test views.
🛠 Changed
- ModelUtil Relation Detection Fix:
- Corrected relation ordering in
ModelUtil.get_select_relateds():- Now detects
ForwardOneToOneDescriptorbeforeForwardManyToOneDescriptorfor buildingselect_relatedlists.
- Now detects
-
Ensures one-to-one relations are properly included in query optimizations.
-
Documentation Updates:
- Extended
docs/api/views/mixins.mdwith a new section forRelationFilterViewSetMixin. - Added examples showing how to configure
relations_filtersand resulting query behavior.
🐛 Fixed
- ModelUtil Primary Key Type Error Handling:
ModelUtil.pk_field_typenow raises a clearConfigErrorwhen encountering unknown primary key field types.-
Error message explicitly reports unsupported field type and suggests missing mapping in
ninja.orm.fields.TYPES. -
ModelUtil Configuration Edge Cases:
-
ModelUtilnow raisesConfigErrorwhen instantiated with aModelSerializermodel and an explicitserializer_classat the same time, avoiding ambiguous configuration. -
ORJSON Renderer Primitive Handling:
- ORJSON renderer now correctly handles non-dict payloads (strings, lists, primitives) without assuming
.items()presence. -
Added coverage for list and primitive responses to ensure consistent rendering behavior.
-
Async JWT Auth Robustness:
AsyncJwtBearer.authenticatenow safely handles invalid or malformed tokens wherejwt.decoderaisesValueError, returningFalseinstead of propagating the exception.- Base
auth_handlerpath verified to returnNonewhen not overridden, and mandatory claims validation now preserves pre-setissandaudvalues.
🧪 Tests
- New Test Suites for Edge Cases:
ModelUtilConfigErrorTestCaseto validateConfigErrorraising when mixingModelSerializermodel andserializer_class.ModelUtilPkFieldTypeTestCaseto ensure unknown PK types triggerConfigErrorwith informative message.- `ModelUtilObjectsQueryDefaultTest
v2.9.0
Jan 14, 2026Release Notes
[v2.9.0] - 2026-01-14
✨ Added
- Detail-Specific Query Optimizations:
- New
QuerySet.detailconfiguration for detail-specificselect_relatedandprefetch_related - New
serializable_detail_fieldsproperty onModelUtilfor accessing detail-specific fields - New
_get_serializable_field_names()helper method for DRY field retrieval -
New
DETAILscope added toQueryUtilBaseScopesSchema -
Fallback Mechanism for Detail Schema:
generate_detail_s()now falls back to read schema when noDetailSerializeris definedget_fields("detail")falls back to read fields when no detail fields are declared_get_read_optimizations("detail")falls back toQuerySet.readwhenQuerySet.detailis not defined
🛠 Changed
- API Parameter Change:
is_for_read→is_for: - Renamed
is_for_read: boolparameter tois_for: Literal["read", "detail"] | Noneacross allModelUtilmethods:get_objects()get_object()read_s()list_read_s()_get_base_queryset()_apply_query_optimizations()_serialize_queryset()_serialize_single_object()_handle_query_mode()_read_s()
-
This enables explicit control over which optimization strategy to use
-
Query Optimization Methods Now Accept
is_forParameter: get_select_relateds(is_for: Literal["read", "detail"] = "read")get_reverse_relations(is_for: Literal["read", "detail"] = "read")-
_get_read_optimizations(is_for: Literal["read", "detail"] = "read") -
APIViewSet Retrieve Endpoint:
- Now uses
is_for="detail"whenschema_detailis available -
Falls back to
is_for="read"when no detail schema is configured -
Code Formatting Improvements:
- Reformatted multi-line tuples in
_is_reverse_relation() - Reformatted conditional in
_warn_missing_relation_serializer() - Reformatted error message in
get_schema_out_data()
🐛 Fixed
- Query Optimization Fallback Bug:
- Fixed
_get_read_optimizations()to fall back toreadconfig whendetailconfig is not defined - Previously returned empty
ModelQuerySetSchema()whenQuerySet.detailwas missing, losing all optimizations
📝 Documentation
- ModelUtil Documentation (docs/api/models/model_util.md):
- Updated all method signatures from
is_for_read: booltois_for: Literal["read", "detail"] | None - Added
QuerySet.detailconfiguration example - Added
serializable_detail_fieldsproperty documentation - Updated examples to show
is_for="read"andis_for="detail"usage -
Added fallback behavior notes for detail optimizations
-
ModelSerializer Documentation (docs/api/models/model_serializer.md):
- Added Fallback Behavior note in
DetailSerializersection - Updated
generate_detail_s()comment to indicate fallback to read schema -
Updated fields table to mention fallback behavior
-
Serializer Documentation (docs/api/models/serializers.md):
- Added
QuerySet.detailconfiguration example - Added explanation of how each QuerySet config is applied (
read,detail,queryset_request,extras)
🧪 Tests
- Updated Test Cases:
- Updated all
is_for_read=Truetois_for="read"across test files - Updated all
is_for_read=Falsetois_for=Noneacross test files - Renamed
test_generate_detail_schema_returns_none_when_not_configuredtotest_generate_detail_schema_falls_back_to_read_when_not_configured -
Updated
test_fallback_to_schema_out_when_no_detailtotest_detail_schema_falls_back_to_read_schema -
New Test Cases:
DetailFieldsModelSerializer- Test model with different read vs detail fields including a relationModelUtilIsForDetailTestCase- Tests foris_for='detail'parameter:test_serializable_fields_returns_read_fields()test_serializable_detail_fields_returns_detail_fields()test_get_select_relateds_read_no_relations()test_get_select_relateds_detail_includes_relation()test_apply_query_optimizations_read_vs_detail()test_get_serializable_field_names_read()test_get_serializable_field_names_detail()
ReadOnlyQuerySetModelSerializer- Test model withQuerySet.readbut noQuerySet.detailModelUtilOptimizationFallbackTestCase- Tests for optimization fallback behavior:test_get_read_optimizations_read()test_get_read_optimizations_detail_falls_back_to_read()test_apply_query_optimizations_detail_uses_read_fallback()
🔧 Internal Changes
- BaseSerializer Changes:
- Added
detail = ModelQuerySetSchema()to innerQuerySetclass -
Added fallback logic in
get_fields()for detail type -
QueryUtilBaseScopesSchema Changes:
-
Added
DETAIL: str = "detail"scope constant -
QueryUtil Changes:
- Added
detail_configproperty for accessing detail query configuration
🚀 Use Cases & Examples
Detail-Specific Query Optimizations
from ninja_aio.models import ModelSerializer
from ninja_aio.schemas.helpers import ModelQuerySetSchema
class Article(ModelSerializer):
title = models.CharField(max_length=200)
summary = models.TextField()
content = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
tags = models.ManyToManyField(Tag)
comments = models.ManyToManyField(Comment)
class ReadSerializer:
# List view: minimal fields
fields = ["id", "title", "summary", "author"]
class DetailSerializer:
# Detail view: all fields including expensive relations
fields = ["id", "title", "summary", "content", "author", "tags", "comments"]
class QuerySet:
# Optimizations for list endpoint
read = ModelQuerySetSchema(
select_related=["author"],
prefetch_related=[],
)
# Optimizations for retrieve endpoint (more aggressive prefetching)
detail = ModelQuerySetSchema(
select_related=["author", "author__profile"],
prefetch_related=["tags", "comments", "comments__author"],
)
Behavior:
- GET /articles/ uses QuerySet.read optimizations (light prefetching)
- GET /articles/{pk} uses QuerySet.detail optimizations (full prefetching)
Fallback Behavior
class Article(ModelSerializer):
class ReadSerializer:
fields = ["id", "title", "content"]
class QuerySet:
read = ModelQuerySetSchema(
select_related=["author"],
prefetch_related=["tags"],
)
# No detail config - will fall back to read!
# Both list and retrieve use QuerySet.read optimizations
# generate_detail_s() returns same schema as generate_read_s()
Using is_for Parameter Directly
from ninja_aio.models import ModelUtil
util = ModelUtil(Article)
# For list operations
qs = await util.get_objects(request, is_for="read")
# For single object retrieval
obj = await util.get_object(request, pk=1, is_for="detail")
# For serialization
data = await util.read_s(schema, request, instance=obj, is_for="detail")
items = await util.list_read_s(schema, request, instances=qs, is_for="read")
🔍 Migration Guide
Breaking Change: is_for_read → is_for
If you call ModelUtil methods directly with is_for_read, update to use is_for:
# Before (v2.8.0)
await util.get_objects(request, is_for_read=True)
await util.get_object(request, pk=1, is_for_read=True)
await util.read_s(schema, request, instance=obj, is_for_read=True)
# After (v2.9.0)
await util.get_objects(request, is_for="read")
await util.get_object(request, pk=1, is_for="detail")
await util.read_s(schema, request, instance=obj, is_for="detail")
Mapping:
| Old Parameter | New Parameter |
|---------------|---------------|
| is_for_read=True | is_for="read" (for list) or is_for="detail" (for retrieve) |
| is_for_read=False | is_for=None |
Adding Detail-Specific Optimizations
# Before (v2.8.0) - Same optimizations for list and retrieve
class QuerySet:
read = ModelQuerySetSchema(
select_related=["author"],
prefetch_related=["tags", "comments"], # Always loaded!
)
# After (v2.9.0) - Different optimizations per operation
class QuerySet:
read = ModelQuerySetSchema(
select_related=["author"],
prefetch_related=[], # Light for list
)
detail = ModelQuerySetSchema(
select_related=["author", "author__profile"],
prefetch_related=["tags", "comments"], # Full for retrieve
)
📊 Performance Benefits
| Scenario | Without Detail Config | With Detail Config |
|---|---|---|
| List 100 articles | Prefetches tags + comments for all | Only prefetches what's needed for list |
| Retrieve single | Uses list optimizations | Uses detail-specific optimizations |
| N+1 queries | May occur if list over-fetches | Optimized per endpoint |
| Memory usage | Higher (unnecessary prefetch) | Optimized per operation |
⚠️ Important Notes
- Breaking Change:
is_for_read: boolparameter renamed tois_for: Literal["read", "detail"] | None - Fallback Behavior: All fallbacks are automatic - no configuration needed for backward compatibility
- QuerySet.detail: Optional - falls back to
QuerySet.readif not defined - DetailSerializer fields: Optional - falls back to
ReadSerializerfields if not defined - generate_detail_s(): Now always returns a schema (falls back to read schema)
🔗 Links
Version History
For older versions, please refer to the GitHub releases page.
v2.8.0
Jan 14, 2026Release Notes
[v2.8.0] - 2026-01-14
✨ Added
- Detail Schema Support for Retrieve Endpoints:
- New
DetailSerializerconfiguration class forModelSerializer - New
schema_detailconfiguration option forSerializerMeta class - New
schema_detailattribute onAPIViewSetfor custom detail schemas - New
generate_detail_s()method for generating detail schemas - Retrieve endpoint (
GET /{base}/{pk}) now usesschema_detailwhen available, falling back toschema_out -
Enables performance optimization: minimal fields for list views, full details for single object retrieval
-
serializer_classSupport for M2MRelationSchema: M2MRelationSchemanow acceptsserializer_classparameter for plain Django models- Auto-generates
related_schemafrom the serializer when provided - Alternative to manually providing
related_schemafor plain models - Validation ensures
serializer_classcannot be used whenmodelis already aModelSerializer
🛠 Changed
- APIViewSet Schema Generation:
get_schemas()now returns a 4-tuple:(schema_out, schema_detail, schema_in, schema_update)- New
_get_retrieve_schema()helper method for retrieve endpoint schema selection -
retrieve_view()updated to use detail schema when available -
Refactored
get_schema_out_data()Function: - Extracted helper methods for better code organization:
_is_reverse_relation()- Check if field is a reverse relation_is_forward_relation()- Check if field is a forward relation_warn_missing_relation_serializer()- Emit warning for missing serializer mappings_process_field()- Process single field and determine classification
- Renamed parameter
typetoschema_typeto avoid shadowing built-in - Renamed internal variable
relstoforward_relsfor clarity -
Now accepts
schema_type: Literal["Out", "Detail"]parameter -
Performance Optimization in
_generate_union_schema(): - Fixed double method call issue using walrus operator
-
generate_related_s()now called once per serializer instead of twice -
Updated Type Definitions:
S_TYPESnow includes"detail":Literal["read", "detail", "create", "update"]SCHEMA_TYPESnow includes"Detail":Literal["In", "Out", "Detail", "Patch", "Related"]
📝 Documentation
- ModelSerializer Documentation (docs/api/models/model_serializer.md):
- New DetailSerializer section with complete documentation
- Updated schema generation table to include
generate_detail_s() - Added example showing List vs Detail output differences
-
Updated "Auto-Generated Schemas" to show five schema types
-
Serializer Documentation (docs/api/models/serializers.md):
- Added
schema_detailto Meta configuration options - New "Detail Schema for Retrieve Endpoint" section
-
Updated schema generation examples to include
generate_detail_s() -
APIViewSet Documentation (docs/api/views/api_view_set.md):
- Updated CRUD endpoints table to show retrieve uses
schema_detail - Added
schema_detailto Core Attributes table - New "Detail Schema for Retrieve Endpoint" section with examples
- Updated automatic schema generation section
- Added
serializer_classdocumentation forM2MRelationSchema - Added tabbed examples for
related_schemavsserializer_classusage
🧪 Tests
- New Detail Schema Test Cases:
-
DetailSerializerTestCasein tests/test_serializers.py:test_generate_detail_schema_with_serializer()- Basic detail schema generationtest_generate_detail_schema_returns_none_when_not_configured()- None when not configuredtest_detail_schema_with_relations()- Relations in detail schematest_detail_schema_with_custom_fields()- Custom fields supporttest_detail_schema_with_optionals()- Optional fields support
-
DetailSchemaModelSerializerTestCasein tests/views/test_viewset.py:test_read_schema_has_minimal_fields()- ReadSerializer has minimal fieldstest_detail_schema_has_extended_fields()- DetailSerializer has extended fieldstest_get_retrieve_schema_returns_detail()- Retrieve uses detail schematest_get_schemas_returns_four_tuple()- get_schemas returns 4-tuple
-
DetailSchemaSerializerTestCase- Tests for Serializer class with schema_detail -
DetailSchemaFallbackTestCase- Tests fallback to schema_out when no detail defined -
New M2M serializer_class Test Cases:
M2MRelationSchemaSerializerClassTestCase- Tests M2M with serializer_class-
M2MRelationSchemaValidationTestCase:test_serializer_class_with_plain_model_succeeds()test_model_serializer_auto_generates_related_schema()test_serializer_class_with_model_serializer_raises_error()test_plain_model_without_serializer_class_or_related_schema_raises_error()test_explicit_related_schema_takes_precedence()
-
New Test Model:
TestModelSerializerWithDetailin tests/test_app/models.py-
Demonstrates separate
ReadSerializerandDetailSerializerconfigurations -
Updated Existing Tests:
- All
schemasproperty definitions updated to return 4-tuple format test_get_schemasupdated to expect 4 elements instead of 3- Refactored
ManyToManyAPITestCaseintoTests.BaseManyToManyAPITestCasebase class
🔧 Internal Changes
- Schema Mapping Updates:
_SCHEMA_META_MAPnow includes"detail": "DetailSerializer"for ModelSerializer_SERIALIZER_CONFIG_MAPnow includes"detail": "detail"for Serializer-
_get_serializer_config()updated to handle"detail"case -
ModelSerializer Changes:
- New
DetailSerializerinner class withfields,customs,optionals,excludesattributes _generate_model_schema()updated to handle"Detail"schema type-
Schema naming:
"Out"→{model}SchemaOut,"Detail"→{model}DetailSchemaOut -
Serializer.Meta Changes:
- New
schema_detail: Optional[SchemaModelConfig]attribute -
model_dump()now uses detail schema when available for single object serialization -
M2MRelationSchema Changes:
- New
serializer_class: Optional[SerializerMeta]field validate_related_schema()validator updated to handle serializer_class- ManyToManyAPI updated to pass serializer_class to ModelUtil
🚀 Use Cases & Examples
Detail Schema for Performance Optimization
from ninja_aio.models import ModelSerializer
from django.db import models
class Article(ModelSerializer):
title = models.CharField(max_length=200)
summary = models.TextField()
content = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
tags = models.ManyToManyField(Tag)
view_count = models.IntegerField(default=0)
class ReadSerializer:
# List view: minimal fields for performance
fields = ["id", "title", "summary", "author"]
class DetailSerializer:
# Detail view: all fields including expensive relations
fields = ["id", "title", "summary", "content", "author", "tags", "view_count"]
customs = [
("reading_time", int, lambda obj: len(obj.content.split()) // 200),
]
@api.viewset(model=Article)
class ArticleViewSet(APIViewSet):
pass # Schemas auto-generated from model
Endpoint Behavior:
- GET /articles/ returns [{"id": 1, "title": "...", "summary": "...", "author": {...}}, ...]
- GET /articles/1 returns {"id": 1, "title": "...", "summary": "...", "content": "...", "author": {...}, "tags": [...], "view_count": 1234, "reading_time": 5}
Detail Schema with Serializer Class
from ninja_aio.models import serializers
class ArticleSerializer(serializers.Serializer):
class Meta:
model = models.Article
schema_out = serializers.SchemaModelConfig(
# List view: minimal fields
fields=["id", "title", "summary"]
)
schema_detail = serializers.SchemaModelConfig(
# Detail view: all fields
fields=["id", "title", "summary", "content", "author", "tags"],
customs=[("reading_time", int, lambda obj: len(obj.content.split()) // 200)]
)
@api.viewset(model=models.Article)
class ArticleViewSet(APIViewSet):
serializer_class = ArticleSerializer
M2M with serializer_class
from ninja_aio.models import serializers
from ninja_aio.schemas import M2MRelationSchema
class TagSerializer(serializers.Serializer):
class Meta:
model = Tag
schema_out = serializers.SchemaModelConfig(fields=["id", "name"])
@api.viewset(model=Article)
class ArticleViewSet(APIViewSet):
m2m_relations = [
M2MRelationSchema(
model=Tag, # plain Django model
related_name="tags",
serializer_class=TagSerializer, # auto-generates related_schema
add=True,
remove=True,
get=True,
)
]
🔍 Migration Guide
Using Detail Schemas
No migration required! Detail schema support is fully backward compatible:
# Existing code continues to work (no DetailSerializer = uses schema_out for retrieve)
class Article(ModelSerializer):
class ReadSerializer:
fields = ["id", "title", "content"] # Used for both list AND retrieve
# New: Add DetailSerializer for different retrieve response
class Article(ModelSerializer):
class ReadSerializer:
fields = ["id", "title"] # Used for list only
class DetailSerializer:
fields = ["id", "title", "content", "author", "tags"] # Used for retrieve
Using serializer_class in M2MRelationSchema
# Before (v2.7.0) - Must provide related_schema manually
M2MRelationSchema(
model=Tag,
related_name="tags",
related_schema=TagOut, # Must define this schema manually
)
# After (v2.8.0) - Can use serializer_class instead
M2MRelationSchema(
model=Tag,
related_name="tags",
serializer_class=TagSerializer, # Auto-generates related_schema!
)
Updating Custom ViewSet Subclasses
If you override get_schemas(), update to return 4-tuple:
# Before (v2.7.0)
def get_schemas(self):
return (schema_out, schema_in, schema_update)
# After (v2.8.0)
def get_schemas(self):
return (schema_out, schema_detail, schema_in, schema_update)
🎯 When to Use Detail Schema
- Performance Optimization: Return minimal fields in list views, full details in retrieve
- API Design: Clients get summaries in lists, full objects on individual requests
- Expensive Relations: Avoid loading M2M/reverse relations for list endpoints
- Computed Fields: Only compute expensive fields for single object retrieval
- Bandwidth Optimization: Reduce payload size for list responses
📊 Performance Benefits
| Scenario | Without Detail Schema | With Detail Schema |
|---|---|---|
| List 100 articles | Returns 100 × full content | Returns 100 × summary only |
| Load M2M tags | Loaded for all 100 items | Only loaded for single retrieve |
| Computed fields | Calculated for all items | Only calculated on retrieve |
| Response size | Large (full content) | Optimized per endpoint |
⚠️ Important Notes
- Fallback Behavior: If
DetailSerializer/schema_detailnot defined, retrieve usesschema_out - Schema Generation:
generate_detail_s()returnsNoneif no detail config exists - Backward Compatibility: All existing code works without changes
- 4-Tuple Return:
get_schemas()now returns 4 values instead of 3 - M2M Validation: Cannot use
serializer_classwithModelSerializermodels
🙏 Acknowledgments
This release focuses on:
- Enhanced API design flexibility with separate list/detail schemas
- Performance optimization for list endpoints
- Better M2M relation configuration options
- Improved code organization and maintainability
🔗 Links
📦 Quick Start with Detail Schema
from ninja_aio.models import ModelSerializer
from ninja_aio.views import APIViewSet
from ninja_aio import NinjaAIO
from django.db import models
api = NinjaAIO(title="My API")
# Step 1: Define your model with ReadSerializer and DetailSerializer
class Article(ModelSerializer):
title = models.CharField(max_length=200)
summary = models.TextField()
content = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
tags = models.ManyToManyField(Tag)
class ReadSerializer:
fields = ["id", "title", "summary"] # Minimal for list
class DetailSerializer:
fields = ["id", "title", "summary", "content", "author", "tags"] # Full for retrieve
# Step 2: Create your ViewSet (schemas auto-generated!)
@api.viewset(model=Article)
class ArticleViewSet(APIViewSet):
pass
# That's it! Your API now has optimized list and detail endpoints:
# GET /articles/ → Returns list with minimal fields
# GET /articles/{pk} → Returns single article with all fields
Version History
For older versions, please refer to the GitHub releases page.
v2.7.0
Jan 13, 2026Release Notes
[v2.7.0] - 2026-01-13
✨ Added
- Union Type Support for Polymorphic Relations:
relations_serializersnow acceptsUnion[SerializerA, SerializerB]to handle polymorphic relationships- Enables flexible handling of generic foreign keys, content types, and multi-model relations
- Direct class references:
Union[SerializerA, SerializerB] - String references:
Union["SerializerA", "SerializerB"] - Mixed references:
Union[SerializerA, "SerializerB"] - Absolute import paths:
Union["myapp.serializers.SerializerA", SerializerB] - Lazy resolution of union members supports forward/circular dependencies
-
Schema generator creates union of all possible schemas automatically
-
Absolute Import Path Support for String References:
- String references now support absolute import paths using dot notation
- Example:
"myapp.serializers.UserSerializer"or"users.serializers.UserSerializer" - Enables cross-module serializer references without circular import issues
- Automatic module importing when needed (uses
importlib.import_module()) - Resolves lazily when schemas are generated
- Works seamlessly with Union types
🛠 Changed
- Enhanced Serializer Reference Resolution:
- Now handles Union types by recursively resolving each member
- Handles ForwardRef objects created by string type hints in unions (e.g.,
Union["StringType"]) -
Optimizes single-type unions by returning the single type directly
-
Enhanced Relation Schema Generation:
- Generates union schemas when serializer reference is a Union type
- Maintains full backward compatibility with single serializer references
-
Automatically filters out None schemas from union members
-
Updated Type Hints:
- All serializer methods updated to reflect Union support
- Better type safety for Union[Schema, ...] return values
- Clearer documentation of acceptable input types
📝 Documentation
- Comprehensive Union Types Documentation in docs/api/models/serializers.md:
-
New "Union Types for Polymorphic Relations" section:
- Complete explanation of Union support with real-world examples
- Basic polymorphic example with Video and Image serializers
- All four Union type format variations documented with code samples
- Use cases: polymorphic relations, flexible APIs, gradual migrations, multi-tenant systems
- Complete polymorphic example using Django's GenericForeignKey
- BlogPost/Product/Event example showing complex multi-model relations
-
New "String Reference Formats" section:
- Local class name format:
"ArticleSerializer" - Absolute import path format:
"myapp.serializers.ArticleSerializer" - Requirements and resolution behavior documented
- Cross-module references example with circular dependencies
- Local class name format:
-
Enhanced Configuration Section:
relations_serializersparameter updated to document Union support- Clear explanation: "Serializer class, string reference, or Union of serializers"
- Forward/circular dependencies and polymorphic relations highlighted
- Updated comparison table showing Union support feature
-
Updated Key Features:
- Added Union types for polymorphic relations to key features list
- Updated notes to mention Union type lazy resolution
- Added note about schema generator creating unions
-
Code Examples and Best Practices:
- Video/Image comment example for basic polymorphic relations
- BlogPost/Product/Event example for complex GenericForeignKey usage
- Cross-module circular reference example (Article ↔ User)
- All four Union format variations with syntax examples
🧪 Tests
-
New Comprehensive Test Suite -
UnionSerializerTestCasein tests/test_serializers.py -
Module-Level Test Serializers:
AltSerializer- Alternative serializer with different field set (id, name)AltStringSerializer- String reference test serializer (id, description)MixedAltSerializer- Mixed reference test serializer (id, name, description)LocalTestSerializer- Local reference test serializer (id only)
🔧 Internal Changes
- Python 3.10+ Compatibility Fix:
- Union types created using
Union[tuple]syntax for compatibility - Replaced incompatible
reduce(or_, resolved_types)pattern - Works correctly across Python 3.10, 3.11, 3.12+
- No dependency on
functools.reduceoroperator.or_ -
Uses Python's typing system to expand
Union[tuple]automatically -
Code Organization:
- Extracted string resolution logic into dedicated
_resolve_string_reference()method - Extracted union schema generation into dedicated
_generate_union_schema()method - Improved separation of concerns and code reusability
- Better error messages with full import paths in exceptions
🚀 Use Cases & Examples
Basic Polymorphic Relations
from typing import Union
from ninja_aio.models import serializers
class VideoSerializer(serializers.Serializer):
class Meta:
model = models.Video
schema_out = serializers.SchemaModelConfig(
fields=["id", "title", "duration", "url"]
)
class ImageSerializer(serializers.Serializer):
class Meta:
model = models.Image
schema_out = serializers.SchemaModelConfig(
fields=["id", "title", "width", "height", "url"]
)
class CommentSerializer(serializers.Serializer):
class Meta:
model = models.Comment
schema_out = serializers.SchemaModelConfig(
fields=["id", "text", "content_object"]
)
relations_serializers = {
"content_object": Union[VideoSerializer, ImageSerializer],
}
Cross-Module References
# myapp/serializers.py
class ArticleSerializer(serializers.Serializer):
class Meta:
model = models.Article
schema_out = serializers.SchemaModelConfig(
fields=["id", "title", "author"]
)
relations_serializers = {
"author": "users.serializers.UserSerializer", # Absolute path
}
# users/serializers.py
class UserSerializer(serializers.Serializer):
class Meta:
model = models.User
schema_out = serializers.SchemaModelConfig(
fields=["id", "username", "articles"]
)
relations_serializers = {
"articles": "myapp.serializers.ArticleSerializer", # Circular ref!
}
Generic Foreign Keys
from django.contrib.contenttypes.fields import GenericForeignKey
from typing import Union
class CommentSerializer(serializers.Serializer):
class Meta:
model = Comment
schema_out = serializers.SchemaModelConfig(
fields=["id", "text", "created_at", "content_object"]
)
relations_serializers = {
"content_object": Union[
BlogPostSerializer,
ProductSerializer,
EventSerializer
],
}
🔍 Migration Guide
Using Union Types
No migration needed! Union support is fully backward compatible:
# Existing code continues to work
class MySerializer(serializers.Serializer):
class Meta:
model = MyModel
relations_serializers = {
"author": AuthorSerializer, # ✅ Still works
}
# New Union syntax available
class MySerializer(serializers.Serializer):
class Meta:
model = MyModel
relations_serializers = {
"content": Union[VideoSerializer, ImageSerializer], # ✅ New!
}
Using Absolute Import Paths
Update string references to use absolute paths for cross-module references:
# Before (v2.6.1) - Only local references worked
relations_serializers = {
"author": "AuthorSerializer", # Must be in same module
}
# After (v2.7.0) - Absolute paths supported
relations_serializers = {
"author": "users.serializers.AuthorSerializer", # ✅ Cross-module!
}
String Reference Formats
Both formats are supported:
relations_serializers = {
# Local reference (same module)
"field1": "LocalSerializer",
# Absolute import path (any module)
"field2": "myapp.serializers.RemoteSerializer",
# Union with mixed formats
"field3": Union["LocalSerializer", "myapp.other.RemoteSerializer"],
}
🎯 When to Use Union Types
- Polymorphic Relations: Generic foreign keys, Django ContentType relations
- Flexible APIs: Different response formats based on runtime type
- Gradual Migrations: Transitioning between serializer implementations
- Multi-Tenant Systems: Different serialization per tenant
- Dynamic Content: CMS systems with multiple content types
- Activity Feeds: Mixed content types in single endpoint
📊 Performance Notes
- Lazy Resolution: Union members resolved only when schemas generated (no startup overhead)
- Schema Caching: Generated schemas can be cached for better performance
- Memory Efficient: Only generates schemas for types actually used
- Import Optimization: Absolute paths only import modules when needed
⚠️ Important Notes
- String References: Resolve within same module by default; use absolute paths for cross-module
- Union Schema Generation: Creates union of all possible schemas from union members
- Backward Compatibility: All existing code continues to work without changes
- Python Version: Requires Python 3.10+ (Union syntax compatibility)
- Type Validation: Union types provide type hints but runtime validation depends on your model logic
🙏 Acknowledgments
This release focuses on:
- Enhanced flexibility for polymorphic relationships
- Better support for complex project architectures
- Improved developer experience with cross-module references
- Python 3.10+ compatibility and modern typing features
🔗 Links
📦 Quick Start with Union Types
from typing import Union
from ninja_aio.models import serializers
# Step 1: Define your serializers
class VideoSerializer(serializers.Serializer):
class Meta:
model = Video
schema_out = serializers.SchemaModelConfig(fields=["id", "title", "url"])
class ImageSerializer(serializers.Serializer):
class Meta:
model = Image
schema_out = serializers.SchemaModelConfig(fields=["id", "title", "url"])
# Step 2: Use Union in relations_serializers
class CommentSerializer(serializers.Serializer):
class Meta:
model = Comment
schema_out = serializers.SchemaModelConfig(
fields=["id", "text", "content_object"]
)
relations_serializers = {
"content_object": Union[VideoSerializer, ImageSerializer],
}
# Step 3: Use with APIViewSet (automatic!)
@api.viewset(model=Comment)
class CommentViewSet(APIViewSet):
serializer_class = CommentSerializer
# Union types work automatically!
Version History
For older versions, please refer to the GitHub releases page.