Reusable code#

One of the worst things about the current generation of Python REST frameworks is code re-usability.

  • django-rest-framework is very flexible, but all the flexibility comes from importing fully qualified object’s path strings taken from app’s settings. It is very hard to properly type a code base like this. Using it is also really hard, because you can’t easily navigate in your source code.

  • fastapi does not even offer a way to write reusable code, because it is based on functions, which are really hard to reuse and modify. That’s why you have to copy paste lots of code just to, for example, use the most common things such as JWT auth.

What does django-modern-rest offer instead?

Explicitly abstract controllers#

A controller can have an exact serializer and endpoints, but you might still not want to route it: you only want to reuse it. Declare is_abstract = True explicitly for this:

Run result

$ curl http://127.0.0.1:8000/api/example/ -X GET
"hello from the base controller"

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"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/mycontroller/": {
      "get": {
        "operationId": "getMyControllerApiMycontroller",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "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"
          }
        }
      },
      "summary": "It does not declare `is_abstract`, so it is concrete again."
    }
  }
}

Such a controller does not build any endpoints, they are only created in a concrete context. Subclasses that don’t declare is_abstract themselves are concrete again, even when their base controller is explicitly abstract. So, MyController from the example above does the same GET request as its base, but it can be routed.

Added in version 0.16.0.

Providing extras for @modify and @validate#

Your custom controllers might require custom parameters that users can provide to validate and modify.

To do so, we utilize extras= parameter. By default it is always typed as empty sentinel, because default controller do not allow any extra parameters.

Three steps are needed:

  1. Define a subclass of Extras. Its fields can default to EMPTY if some arguments can be missing

  2. Define build with how to build your value from several configuration layers, you can use global settings there as well

  3. Create typed decorators by passing this class to ModifyEndpoint and ValidateEndpoint

  4. Assign an instance of this class to extras attribute of your controller. It enables extras= for all endpoints of this controller and provides controller-level defaults

build() receives the endpoint layer, which is EMPTY when extras= is not passed, and the controller layer as instances of your class, and returns the resolved value. It is stored inside extras and can be read with of() in a typed way.

First, define the extras model itself:

Then define and use new endpoint decorators:

Real world example: dmr.streaming.modify

Run result

$ curl http://127.0.0.1:8000/api/example/ -X GET
"default response"

$ curl http://127.0.0.1:8000/api/example/ -X POST
"from endpoint"

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"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/apicontroller/": {
      "get": {
        "operationId": "getAPIControllerApiApicontroller",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "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"
          }
        }
      },
      "post": {
        "operationId": "postAPIControllerApiApicontroller",
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "Created"
          },
          "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"
          }
        }
      }
    }
  }
}

This way your controller subtypes can have any extras that you need!

These definitions would only differ in terms of typing. Everything else would work the same way.

Added in version 0.16.0.

Reusable controllers#

We offer a concept of a “reusable controllers”.

To make a reusable controller, you need to provide typing.TypeVar instead of a real BaseSerializer type.

Here’s an example:

This code can work with both pydantic and msgspec as serializers. Let’s try to create two exact controllers with exact serializers:

Run result

$ curl http://127.0.0.1:8000/api/example/ -X GET
{"message":"hello from MsgspecSerializer"}

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"
      },
      "_ResponseBody": {
        "properties": {
          "message": {
            "type": "string"
          }
        },
        "required": [
          "message"
        ],
        "title": "_ResponseBody",
        "type": "object"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/msgspeccontroller/": {
      "get": {
        "operationId": "getMsgspecControllerApiMsgspeccontroller",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/_ResponseBody"
                }
              }
            },
            "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"
          }
        }
      },
      "summary": "This controller will use msgspec for serialization."
    }
  }
}

Basically - we just specify what kind of serializer to use. And that’s it. But, this is just the first step. We can do much more!

Note

Only controllers with an exact serializer and at least one endpoint can be routed. Reusable ones have is_abstract set to True and raise EndpointMetadataError when you call .as_view() on them. Route their subclasses instead.

Added in version 0.16.0.

Tip

Annotate class-level options like responses, auth, parsers, renderers, and throttling in controllers that will be subclassed.

Without an annotation type-checkers infer a fixed-size tuple from the value you assign, and subclasses won’t be able to add or to remove items from it.

Generic parsing and response models#

Next, let’s define a reusable controller that will have:

  • customizable serializer

  • customizable request model

  • customizable response body

Raw endpoints support#

The process will look exactly the same:

Here we use 3 type variables. One of each of the parts we want to customize.

Important part here is that we defined our own abstract convert method to convert unknown request model into an unknown response body.

We would need to implement this method in all of our concrete controllers.

Run result

$ curl http://127.0.0.1:8000/api/example/ -X POST -d '{"username": "sobolevn"}' -H 'Content-Type: application/json'
{"message":"Hello, sobolevn"}

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"
      },
      "_RequestModel": {
        "properties": {
          "username": {
            "type": "string"
          }
        },
        "required": [
          "username"
        ],
        "title": "_RequestModel",
        "type": "object"
      },
      "_ResponseBody": {
        "properties": {
          "message": {
            "type": "string"
          }
        },
        "required": [
          "message"
        ],
        "title": "_ResponseBody",
        "type": "object"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/msgspeccontroller/": {
      "post": {
        "operationId": "postMsgspecControllerApiMsgspeccontroller",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/_RequestModel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/_ResponseBody"
                }
              }
            },
            "description": "Created"
          },
          "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"
          }
        }
      }
    }
  }
}

Note that msgspec and pydantic controllers in this case have completely different request and response bodies and completely different OpenAPI schemas.

We can completely customize each controller and all parsing components and return type validation.

Important

All schema generation and validation rules work the same way for concrete controllers.

We infer the passed values during import time and use real types.

Real endpoints support#

Added in version 0.14.0.

The same would work with endpoints defined with validate decorator.

The logic is the same, but syntax is a bit different.

Tip

By default mypy and other type-checkers won’t allow to write ResponseSpec(_TypeT, status_code=OK), because type vars can’t be used in such places according to the typing spec.

So, we provide dmr.types.safe_typevar() helper to get rid of the type-checking errors.

Here’s how we can do the same example, but with @validate. The reusable part:

And then - implementations:

Run result

$ curl http://127.0.0.1:8000/api/example/ -X POST -d '{"username": "sobolevn"}' -H 'Content-Type: application/json'
{"message":"Hello, sobolevn"}

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"
      },
      "_RequestModel": {
        "properties": {
          "username": {
            "type": "string"
          }
        },
        "required": [
          "username"
        ],
        "title": "_RequestModel",
        "type": "object"
      },
      "_ResponseBody": {
        "properties": {
          "message": {
            "type": "string"
          }
        },
        "required": [
          "message"
        ],
        "title": "_ResponseBody",
        "type": "object"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/msgspeccontroller/": {
      "post": {
        "operationId": "postMsgspecControllerApiMsgspeccontroller",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/_RequestModel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/_ResponseBody"
                }
              }
            },
            "description": "Created"
          },
          "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"
          }
        }
      }
    }
  }
}

This way offers you more control over the response headers, cookies, etc. Choose the one that fits best of the job.

Type variable defaults#

Added in version 0.16.0.

Type variables can have defaults, as described in PEP 696. We use them when a subclass does not provide some of the type args.

There are three ways to define them:

  • typing_extensions.TypeVar('_ModelT', default=MyModel) on any version

  • typing.TypeVar with the same default= argument on 3.13 and above

  • the native class Reusable[_ModelT = MyModel] syntax on 3.13 and above

The request model is now optional for the subclasses:

Run result

$ curl http://127.0.0.1:8000/api/example/ -X POST -d '{"first_name": "Nikita", "last_name": "Sobolev"}' -H 'Content-Type: application/json'
{"first_name":"Nikita","last_name":"Sobolev"}

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {
      "DefaultRequestModel": {
        "properties": {
          "first_name": {
            "title": "First Name",
            "type": "string"
          },
          "last_name": {
            "title": "Last Name",
            "type": "string"
          }
        },
        "required": [
          "first_name",
          "last_name"
        ],
        "title": "DefaultRequestModel",
        "type": "object"
      },
      "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"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/pydanticcontroller/": {
      "post": {
        "operationId": "postPydanticControllerApiPydanticcontroller",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DefaultRequestModel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DefaultRequestModel"
                }
              }
            },
            "description": "Created"
          },
          "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"
          }
        }
      },
      "summary": "The request model is not given, so it is `DefaultRequestModel`."
    }
  }
}

Defaults can also point to other type variables: _ResponseBodyT = TypeVar('_ResponseBodyT', default=_RequestModelT) means “the response body is the request model, unless told otherwise”.

Serializers can have defaults as well. Then a subclass that passes no type args at all is a concrete controller, because it has an exact serializer:

Run result

$ curl http://127.0.0.1:8000/api/example/ -X POST -d '{"first_name": "Nikita", "last_name": "Sobolev"}' -H 'Content-Type: application/json'
{"first_name":"Nikita","last_name":"Sobolev"}

Important

A default only applies to the subclasses, never to the reusable controller that declares it. ReusableController above still has is_abstract set to True and cannot be routed, even though every one of its type variables has a default.

This is the same rule as everywhere else: we only build endpoints for concrete controllers, and a type variable is not an exact type.

Note

A type variable without a default is still required. Controllers that don’t have an exact serializer stay abstract, as always.

Write the subclass when you have anything else to say: a setting to change, a hook to redefine, or a name to route several times.

Lazy reusable endpoints#

In some cases you need to do even more than request / response payload modifications. Sometimes, you need to change the status code, header and cookie specs, maybe even auth or throttling definitions.

Important

We only create endpoints when concrete controller is built. We never create endpoins definitions for reusable controllers. So, all endpoints are always created in the correct - final - context.

To use the full customization, we provide:

Here’s how it works:

What happens here?

  1. We define a reusable controller with lazy endpoint specification

  2. We define lazy_spec classmethod that will provide the actual decorator during the child - final - controller build time

  3. We use class-level API to define constants that people can modify in their child - final - controllers if needed. But, the default implementation would work the way we described it

Notice that we use special types to define the return type from the lazy_spec classmethod. Here are all of them, choose the one for your task:

Type name

Original decorator

What it does

ModifyAnyCallable

modify

Creates a decorator for endpoints without sync / async specifics

ModifySyncCallable

modify

Creates a decorator for sync endpoints

ModifyAsyncCallable

modify

Creates a decorator for async endpoints

ValidateAnyCallable

validate

Creates a decorator for endpoints without sync / async specifics

ValidateSyncCallable

validate

Creates a decorator for sync endpoints

ValidateAsyncCallable

validate

Creates a decorator for async endpoints

Basically, there are several major rules:

  1. Prefer defining decorators that do not care about sync / async code, use ModifyAnyCallable and ValidateAnyCallable by default

  2. If you need sync / async details like error_handler, auth, or throttling - then use exact type for sync / async decorator of your choice

  3. Do not mix specs for @validate and @modify, it will be a type error

Customizing definitions#

Now, let’s use the reusable controller we defined above, but we would customize the response status code, just as an example of power that we have:

Run result

$ curl http://127.0.0.1:8000/api/example/ -D - -X GET
HTTP/1.1 226 IM Used
date: Wed, 30 Sep 2026 23:19:47 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 7
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

"login"

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"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/ourlogincontroller/": {
      "get": {
        "operationId": "getOurLoginControllerApiOurlogincontroller",
        "responses": {
          "226": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "IM Used"
          },
          "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"
          }
        }
      }
    }
  }
}

Notice that the response code would be changed in both the spec and runtime. And, of course, you can combine this approach with generic serializer, or request and response payloads. Giving the full control over code reuse.

Overriding definitions#

To finish this example off, we would completely override the spec in a child controller. Sometimes users might want to do that, for example: to provide auth or custom OpenAPI spec. But, user is free to modify any parts of the spec, if needed.

Run result

$ curl http://127.0.0.1:8000/api/example/ -D - -X GET
HTTP/1.1 226 IM Used
date: Wed, 30 Sep 2026 23:19:48 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 7
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

"login"

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"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/ourlogincontroller/": {
      "get": {
        "operationId": "getOurLoginControllerApiOurlogincontroller",
        "responses": {
          "226": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "IM Used"
          },
          "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"
          }
        },
        "tags": [
          "custom_openapi_meta"
        ]
      }
    }
  }
}

Note

Notice that lazy_spec classmethod is resolve from the final controller, not the one that was used during the decoration time.

@classmethod is preferable over lambda functions, because they provide easier override API and they are fully typed. The controller argument of a lambda is typed as type[Any], so attribute access on it is not checked by type checkers.

Where is it actually helpful in practice?#

We use this feature a lot in the pre-defined views we provide with the framework.

For example, we use this in JWT Auth obtain views:

  1. ObtainTokensSyncController for sync controllers

  2. ObtainTokensAsyncController for async controllers

Usage example:

Run result

$ curl http://127.0.0.1:8000/api/auth/ -X POST -d '{"username": "test_user", "password": "password"}' -H 'Content-Type: application/json'
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxNzkwODk2NzkwLCJpYXQiOjE3OTA4MTAzOTAsImp0aSI6IjQ2ZWJhODNkZjNjYjRhYWI5MmFjMWNkYTZlYjkxYmUwIiwiZXh0cmFzIjp7InR5cGUiOiJhY2Nlc3MifX0.Tq-ZR5dCJLEy-EAtAOEUgfevtLe4UmmFLOvXWJmh4nc","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxNzkxNjc0MzkwLCJpYXQiOjE3OTA4MTAzOTAsImp0aSI6Ijk4OTE0ZmU1NjM5NjQxYTFhZDIxYWRlMGFlMWUxNTc4IiwiZXh0cmFzIjp7InR5cGUiOiJyZWZyZXNoIn19.enPuJ9zdXptEOBgsGRIsl9_12vTsVqNABx2KNTPOoyg"}

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"
      },
      "ObtainTokensPayload": {
        "description": "Payload for default version of a jwt request body.\n\nIs also used as kwargs for :func:`django.contrib.auth.authenticate`.",
        "properties": {
          "password": {
            "title": "Password",
            "type": "string"
          },
          "username": {
            "title": "Username",
            "type": "string"
          }
        },
        "required": [
          "username",
          "password"
        ],
        "title": "ObtainTokensPayload",
        "type": "object"
      },
      "ObtainTokensResponse": {
        "description": "Default response type for refresh token endpoint.",
        "properties": {
          "access_token": {
            "title": "Access Token",
            "type": "string"
          },
          "refresh_token": {
            "title": "Refresh Token",
            "type": "string"
          }
        },
        "required": [
          "access_token",
          "refresh_token"
        ],
        "title": "ObtainTokensResponse",
        "type": "object"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/obtainaccessandrefreshsynccontroller/": {
      "post": {
        "operationId": "postObtainAccessAndRefreshSyncControllerApiObtainaccessandrefreshsynccontroller",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ObtainTokensPayload"
              }
            }
          },
          "description": "Payload for default version of a jwt request body.\n\nIs also used as kwargs for :func:`django.contrib.auth.authenticate`.",
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ObtainTokensResponse"
                }
              }
            },
            "description": "OK",
            "headers": {
              "Cache-Control": {
                "description": "Credentials must not be stored in any cache.",
                "required": true,
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when request components cannot be parsed"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Unauthorized"
          },
          "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"
          }
        },
        "summary": "By default tokens are acquired on post."
      }
    }
  }
}

Why is it useful?

  1. We can work with any serializer

  2. We can change our request payload to be whatever we need, it would be correctly rendered in the final OpenAPI schema

  3. We can change the response schema, which would also be correctly rendered in the OpenAPI

This feature allows us to have type-safe and OpenAPI-first approach to code reusability, great DX, and Python-native abstractions.

Users / plugin developers can do the same to provide universal customizable controllers.