External views#

Added in version 0.13.0.

django-modern-rest is build around several pure-Django concepts:

  • Controller which is a subclass of View to define API views

  • Router to manipulate URLPattern objects and URLs

  • OpenAPI dataclass to store OpenAPI spec near the view

So, any Django-compatible View objects can be used with django-modern-rest with the user-provided OpenAPI schema.

Important

'django-modern-rest[pydantic]' must be installed to use external views feature. load_schema() requires it to deserialize OpenAPI objects.

How it works?#

Just like good old Django!

We don’t touch the existing view logic / OpenAPI metadata in any way.

The view itself can do any validation or logic, parse / serialize objects in any way. We also just include the existing OpenAPI metadata, without any logic or modifications.

Note

However, if you define top-level error handlers with build_404_handler() and build_500_handler(), it would still affect the view, when these errors happen.

OpenAPI#

Imagine that you already have an OpenAPI spec, it might be from another library, old project, legacy service, etc.

But, we can continue to use it. Let’s say you have an existing service that returns you random numbers. Here’s its spec:

Next, let’s show how we can adapt existing pure-Django API views, attach this to existing schema as:

  • both functional,

  • and class-based.

Tip

If external OpenAPI has validation issues, you might want to disable the OpenAPI validation process for the whole schema. Pass skip_validation=False to the converter methods. See dmr.openapi.openapi.OpenAPI.convert() and dmr.openapi.views.base.OpenAPIView.as_view().

The main feature that allows us to do this is dmr.routing.external_path(). It follows the same API design as django.urls.path(), but also requires openapi kw-only parameter to be passed together with other regular path() parameters.

Functional example#

Let’s start with functions. Our previously described random number API service can be a function:

Run result

$ curl http://127.0.0.1:8000/api/number/ -X GET
5

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {},
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/number/": {
      "get": {
        "deprecated": false,
        "operationId": "getNumber",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "number"
                }
              }
            },
            "description": "A number"
          }
        }
      }
    }
  }
}

Notice that /api/numbers path item definition from openapi.yml was inserted into our final schema as-is.

Class example#

Now, the same, but with a class:

Run result

$ curl http://127.0.0.1:8000/api/number/ -X GET
5

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {},
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/number/": {
      "get": {
        "deprecated": false,
        "operationId": "getNumber",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "number"
                }
              }
            },
            "description": "A number"
          }
        }
      }
    }
  }
}

It can be any View compatible class!

Mixing URLs#

Since we just work with regular Django URLs, you can mix django.urls.path() and dmr.routing.external_path() items.

Order of passed urls is preserved:

Run result

$ curl http://127.0.0.1:8000/api/dmr-number/ -X GET
5

$ curl http://127.0.0.1:8000/api/number/ -X GET
7

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"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/dmr-number/": {
      "get": {
        "deprecated": false,
        "operationId": "getNumbercontrollerApiDmrNumber",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "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"
          }
        }
      }
    },
    "/api/number/": {
      "get": {
        "deprecated": false,
        "operationId": "getNumber",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "number"
                }
              }
            },
            "description": "A number"
          }
        }
      }
    }
  }
}

Note that dmr.controller.Controller items will generate its own OpenAPI PathItem schema. While external_path() would just insert the existing OpenAPI metadata as-is.

Registering OpenAPI schemas#

Now, we have a new OpenAPI file. It has several changes from the first one:

  • It contains int path parameters start and end

  • It has a schema component with a $ref as a return, so we would have to register in the final spec

  • It has top-level tags definition that we also want to copy to the final spec

It does the same thing, but has two path parameters and also defines OpenAPI schemas that we need to re-register, so they would be available in our final spec:

To register OpenAPI schema components and tags, we can use dmr.openapi.OpenAPIConfig customization:

Run result

$ curl http://127.0.0.1:8000/api/number/1/5/ -X GET
3

$ curl http://127.0.0.1:8000/api/number/regular-django/url-error/ -D - -X GET
HTTP/1.1 404 Not Found
date: Thu, 13 Aug 2026 14:36:46 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 56
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{"detail":[{"msg":"Page not found","type":"not_found"}]}

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {
      "RandomNumber": {
        "properties": {
          "value": {
            "type": "number"
          }
        },
        "required": [
          "value"
        ],
        "type": "object"
      }
    }
  },
  "info": {
    "title": "New Random Number API",
    "version": "0.0.1"
  },
  "openapi": "3.1.0",
  "paths": {
    "/api/number/{start}/{end}/": {
      "get": {
        "deprecated": false,
        "operationId": "getRandomNumber",
        "parameters": [
          {
            "deprecated": false,
            "in": "path",
            "name": "start",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "deprecated": false,
            "in": "path",
            "name": "end",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RandomNumber"
                }
              }
            },
            "description": "A random number between start and end"
          }
        },
        "tags": [
          "random"
        ]
      }
    }
  },
  "tags": [
    {
      "description": "Operations for generating random numbers",
      "name": "random"
    }
  ]
}

There are several rules on how we merge the pre-defined config with the automatically generated one:

  • When trying to redefine an existing component by name, ValueError is raised

  • We merge all components from all possible types

  • We can’t detect unused ones, so we merge all, even if some of them are not used

If you have several Components definitions, you can pass a list of them to the dmr.openapi.OpenAPIConfig instance. All of them will be merged into the final spec correctly.

See OpenAPI for more possible customizations.

Excluding external views from OpenAPI#

It might be important to add a private API endpoint, without registering it in the final OpenAPI spec.

Some endpoints might not even have OpenAPI in the first place!

To achieve this, pass None instead of the PathItem schema:

Run result

$ curl http://127.0.0.1:8000/api/number/ -X GET
10

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {},
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {}
}

The view will still work as expected, but won’t be present in the spec.

See Excluding views from OpenAPI for more details about excluding regular views from the OpenAPI.

Real world use-cases#

For example, one can reuse:

  • django-allauth headless views that are built to be used by the pure Django framework

  • Any APIView or GenericAPIView objects from django-rest-framework

  • Any ControllerBase objects from django-ninja-extra

  • And many more!