Integrations

Serializing QuerySets into models

Django is built around its QuerySet type. Of course, we have to make sure that it is supported.

Let’s say you have these models that you already work with:

Now, let’s create an API that will work with your models. To do that the first thing you need to do is to create your API serializers / deserializers.

While it may seems that this is a redundant duplication of code, and that it should be possible to build serialization schemas out of Django models, but that’s actually the opposite.

Because models and serialization schemes must change independenly. Otherwise, your API would be a mess and will change unexpectedly, when you create a new migration. This problem happened to me too many times.

Important

Models and QuerySets can’t be serialized to json by default. This is a design choice, this is a feature.

Why?

Because Models and QuerySets are not designed for serialization, they are designed for the database access. Mixing these two layers will complicate, not simplify, your app.

Now, let’s create a service to build your model instances:

Here’s how the final Controller would look like:

Now you have your REST API that returns fully typed model responses and can work with QuerySet and Model instances.

django-mantle

If you want to automate this part and automatically convert QuerySet into typed models, you can use django-mantle which is built just for this purpose:

Run result

$ curl http://127.0.0.1:8000/api/users/ -X GET
[{"email":"test@example.com","is_active":true,"username":"test_user"}]

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {
      "ErrorDetail": {
        "description": "Base schema for error details description.",
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string"
                }
              ]
            },
            "type": "array"
          },
          "msg": {
            "type": "string"
          },
          "type": {
            "type": "string"
          }
        },
        "required": [
          "msg"
        ],
        "title": "ErrorDetail",
        "type": "object"
      },
      "ErrorModel": {
        "description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ErrorDetail"
            },
            "type": "array"
          }
        },
        "required": [
          "detail"
        ],
        "title": "ErrorModel",
        "type": "object"
      },
      "_UserModel": {
        "properties": {
          "email": {
            "type": "string"
          },
          "is_active": {
            "type": "boolean"
          },
          "username": {
            "type": "string"
          }
        },
        "required": [
          "username",
          "email",
          "is_active"
        ],
        "title": "_UserModel",
        "type": "object"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/userscontroller/": {
      "get": {
        "deprecated": false,
        "operationId": "getUserscontrollerApiUserscontroller",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/_UserModel"
                  },
                  "type": "array"
                }
              }
            },
            "description": "OK"
          },
          "406": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when provided `Accept` header cannot be satisfied"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when returned response does not match the response schema"
          }
        }
      }
    }
  }
}

CSRF

Django supports Cross Site Request Forgery protection.

By default we exempt all controllers from CSRF checks, unless:

  1. csrf_exempt is set to False for a specific controller

  2. Endpoints protected by DjangoSessionSyncAuth or DjangoSessionAsyncAuth will require CSRF as well. Because using Django sessions without CSRF is not secure

Bring your own DI

We don’t have any opinions about any DI that you can potentially use. Because django-modern-rest is compatible with any of the existing ones.

Use any DI that you already have or want to use with django.

Try any of these officially recommended tools:

Or any other one that suits your needs :)

Typing

Django does not have type annotations, by default, so mypy won’t type check Django apps by default. But, when django-stubs is installed, type checking starts to shine.

So, when you use mypy, you will need to install django-stubs together with django-modern-rest to have the best type checking experience.

This package is included in pyright by default. No actions are required.

We check django-modern-rest code with mypy and pyright strict modes in CI, so be sure to have the best typing possible.

See our project template to learn how typing works, how mypy is configured, how django-stubs is used.

Pagination

We don’t ship our own pagination. We (as our main design goal suggests) provide support for any existing pagination plugin for Django. Including the built-in django.core.paginator.Paginator.

To do so, we only provide metadata for the default pagination:

Run result

$ curl http://127.0.0.1:8000/api/users/ -X GET
{"count":3,"num_pages":2,"per_page":2,"page":{"number":1,"object_list":[{"email":"one@example.com"},{"email":"two@example.com"}]}}

$ curl 'http://127.0.0.1:8000/api/users/?page=2' -X GET
{"count":3,"num_pages":2,"per_page":2,"page":{"number":2,"object_list":[{"email":"three@example.com"}]}}

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {
      "ErrorDetail": {
        "description": "Base schema for error details description.",
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string"
                }
              ]
            },
            "title": "Loc",
            "type": "array"
          },
          "msg": {
            "title": "Msg",
            "type": "string"
          },
          "type": {
            "title": "Type",
            "type": "string"
          }
        },
        "required": [
          "msg"
        ],
        "title": "ErrorDetail",
        "type": "object"
      },
      "ErrorModel": {
        "description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ErrorDetail"
            },
            "title": "Detail",
            "type": "array"
          }
        },
        "required": [
          "detail"
        ],
        "title": "ErrorModel",
        "type": "object"
      },
      "Page__User_": {
        "properties": {
          "number": {
            "title": "Number",
            "type": "integer"
          },
          "object_list": {
            "items": {
              "$ref": "#/components/schemas/_User"
            },
            "title": "Object List",
            "type": "array"
          }
        },
        "required": [
          "number",
          "object_list"
        ],
        "title": "Page",
        "type": "object"
      },
      "Paginated": {
        "properties": {
          "count": {
            "title": "Count",
            "type": "integer"
          },
          "num_pages": {
            "title": "Num Pages",
            "type": "integer"
          },
          "page": {
            "$ref": "#/components/schemas/Page__User_"
          },
          "per_page": {
            "title": "Per Page",
            "type": "integer"
          }
        },
        "required": [
          "count",
          "num_pages",
          "per_page",
          "page"
        ],
        "title": "Paginated",
        "type": "object"
      },
      "_User": {
        "properties": {
          "email": {
            "title": "Email",
            "type": "string"
          }
        },
        "required": [
          "email"
        ],
        "title": "_User",
        "type": "object"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/userscontroller/": {
      "get": {
        "deprecated": false,
        "operationId": "getUserscontrollerApiUserscontroller",
        "parameters": [
          {
            "deprecated": false,
            "in": "query",
            "name": "page_size",
            "schema": {
              "title": "Page Size",
              "type": "integer"
            }
          },
          {
            "deprecated": false,
            "in": "query",
            "name": "page",
            "schema": {
              "title": "Page",
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Paginated"
                }
              }
            },
            "description": "OK"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when request components cannot be parsed"
          },
          "406": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when provided `Accept` header cannot be satisfied"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when returned response does not match the response schema"
          }
        }
      }
    }
  }
}

If you are using a different pagination system, you can define your own metadata / models and use them with our framework.

class dmr.pagination.Paginated(*, count: int, num_pages: int, per_page: int, page: Page[_ModelT])[source]

Helper type to serialize the default Paginator object.

Django already ships a pagination system, we don’t want to replicate it. So, we only provide metadata. See django.core.paginator.Paginator for the exact API.

class dmr.pagination.Page(*, number: int, object_list: Sequence[_ModelT])[source]

Default page model for serialization.

Can be used when using pagination with django-modern-rest.

django-filters

No special integration with django-filter is required.

Everything just works.

import django_filters
import pydantic
from dmr import Controller, Query
from dmr.plugins.pydantic import PydanticSerializer

from your_app.models import User

class UserFilter(django_filters.FilterSet):
    class Meta:
        model = User
        fields = ('is_active',)

# Create query model for better docs:
class QueryModel(pydantic.BaseModel):
    is_active: bool

class UserModel(pydantic.BaseModel):
    username: str
    email: str
    is_active: bool

class UserListController(
    Controller[PydanticSerializer],
    Query[QueryModel],
):
    def get(self) -> list[UserModel]:
        # Still pass `.GET` for API compatibility:
        user_filter = UserFilter(
             self.request.GET,
             queryset=User.objects.all(),
        )
        return [
            UserModel.model_validate(user, from_attributes=True)
            for user in user_filter.qs
        ]

CORS Headers

No special integration with django-cors-headers is required.

Everything just works.

Conditional requests (ETag)

Django has built-in support for conditional request processing (If-None-Match, If-Modified-Since, 304 Not Modified):

With django-modern-rest you can integrate it via wrap_middleware() and django.views.decorators.http.condition().

Run result

$ curl http://127.0.0.1:8000/api/example/1/ -D - -X GET
HTTP/1.1 200 OK
date: Sun, 29 Mar 2026 18:52:52 GMT
server: uvicorn
Content-Type: application/json
ETag: "user-1-2026-03-23T12:30:00+00:00"
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 80
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{"message":"Fresh content for user #1","updated_at":"2026-03-23T12:30:00+00:00"}

$ curl http://127.0.0.1:8000/api/example/1/ -D - -X GET -H 'If-None-Match: "user-1-2026-03-23T12:30:00+00:00"'
HTTP/1.1 304 Not Modified
date: Sun, 29 Mar 2026 18:52:52 GMT
server: uvicorn
ETag: "user-1-2026-03-23T12:30:00+00:00"
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 0
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin


$ curl http://127.0.0.1:8000/api/example/2/ -D - -X GET
HTTP/1.1 200 OK
date: Sun, 29 Mar 2026 18:52:52 GMT
server: uvicorn
Content-Type: application/json
ETag: "user-2-2026-03-24T09:15:00+00:00"
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 80
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{"message":"Fresh content for user #2","updated_at":"2026-03-24T09:15:00+00:00"}