External views#
Added in version 0.13.0.
django-modern-rest is build around several pure-Django concepts:
Controllerwhich is a subclass ofViewto define API viewsRouterto manipulateURLPatternobjects and URLsOpenAPIdataclass 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:
1openapi: 3.0.3
2info:
3 title: Number API
4 version: 1.0.0
5paths:
6 /api/number:
7 get:
8 operationId: getNumber
9 responses:
10 '200':
11 description: A number
12 content:
13 application/json:
14 schema:
15 type: number
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:
1import random
2
3from django.http import HttpRequest, JsonResponse
4from django.views.decorators.http import require_GET
5
6from dmr.openapi import build_schema, load_schema
7from dmr.openapi.objects import PathItem
8from dmr.openapi.views import OpenAPIJsonView
9from dmr.routing import Router, external_path, path
10from examples.external_views.read_openapi import read_openapi_yaml
11
12
13@require_GET
14def number(request: HttpRequest) -> JsonResponse:
15 return JsonResponse(random.randint(1, 10), safe=False)
16
17
18# Now, load the schema:
19raw_schema = read_openapi_yaml('openapi.yml')
20
21# Create a router and URL patterns:
22router = Router(
23 'api/',
24 urls=[
25 external_path(
26 'number/',
27 number,
28 name='number',
29 openapi=load_schema(raw_schema['paths']['/api/number'], PathItem),
30 ),
31 ],
32)
33schema = build_schema(router)
34
35urlpatterns = [
36 # Register our router in the final url patterns:
37 router.to_urlpatterns(namespace='api'),
38 # Add swagger:
39 path('docs/openapi.json/', OpenAPIJsonView.as_view(schema), name='openapi'),
40]
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:
1import random
2
3from django.http import HttpRequest, JsonResponse
4from django.views import View
5
6from dmr.openapi import build_schema, load_schema
7from dmr.openapi.objects import PathItem
8from dmr.openapi.views import OpenAPIJsonView
9from dmr.routing import Router, external_path, path
10from examples.external_views.read_openapi import read_openapi_yaml
11
12
13class NumberView(View):
14 def get(self, request: HttpRequest) -> JsonResponse:
15 return JsonResponse(random.randint(1, 10), safe=False)
16
17
18# Now, load the schema:
19raw_schema = read_openapi_yaml('openapi.yml')
20
21# Create a router and URL patterns:
22router = Router(
23 'api/',
24 urls=[
25 external_path(
26 'number/',
27 NumberView.as_view(),
28 name='number',
29 openapi=load_schema(raw_schema['paths']['/api/number'], PathItem),
30 ),
31 ],
32)
33schema = build_schema(router)
34
35urlpatterns = [
36 # Register our router in the final url patterns:
37 router.to_urlpatterns(namespace='api'),
38 # Add swagger:
39 path('docs/openapi.json/', OpenAPIJsonView.as_view(schema), name='openapi'),
40]
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:
1import random
2
3from django.http import HttpRequest, JsonResponse
4from django.views.decorators.http import require_GET
5
6from dmr import Controller
7from dmr.openapi import build_schema, load_schema
8from dmr.openapi.objects import PathItem
9from dmr.openapi.views import OpenAPIJsonView
10from dmr.plugins.pydantic import PydanticFastSerializer
11from dmr.routing import Router, external_path, path
12from examples.external_views.read_openapi import read_openapi_yaml
13
14
15class NumberController(Controller[PydanticFastSerializer]):
16 def get(self) -> int:
17 return random.randint(1, 10)
18
19
20@require_GET
21def number(request: HttpRequest) -> JsonResponse:
22 return JsonResponse(random.randint(1, 10), safe=False)
23
24
25# Now, load the schema:
26raw_schema = read_openapi_yaml('openapi.yml')
27
28# Create a router and URL patterns:
29router = Router(
30 'api/',
31 urls=[
32 path('dmr-number/', NumberController.as_view(), name='dmr_number'),
33 external_path(
34 'number/',
35 number,
36 name='number',
37 openapi=load_schema(raw_schema['paths']['/api/number'], PathItem),
38 ),
39 ],
40)
41schema = build_schema(router)
42
43urlpatterns = [
44 # Register our router in the final url patterns:
45 router.to_urlpatterns(namespace='api'),
46 # Add swagger:
47 path('docs/openapi.json/', OpenAPIJsonView.as_view(schema), name='openapi'),
48]
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
intpath parametersstartandendIt has a schema component with a
$refas a return, so we would have to register in the final specIt 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:
1openapi: 3.1.0
2info:
3 title: Random Number API
4 version: 1.0.0
5tags:
6 - name: random
7 description: Operations for generating random numbers
8paths:
9 /api/random/{start}/{end}:
10 get:
11 operationId: getRandomNumber
12 tags:
13 - random
14 parameters:
15 - name: start
16 in: path
17 required: true
18 schema:
19 type: integer
20 - name: end
21 in: path
22 required: true
23 schema:
24 type: integer
25 responses:
26 '200':
27 description: A random number between start and end
28 content:
29 application/json:
30 schema:
31 $ref: '#/components/schemas/RandomNumber'
32components:
33 schemas:
34 RandomNumber:
35 type: object
36 properties:
37 value:
38 type: number
39 required:
40 - value
To register OpenAPI schema components and tags, we can use
dmr.openapi.OpenAPIConfig customization:
1import random
2
3from django.http import HttpRequest, JsonResponse
4from django.views.decorators.http import require_GET
5
6from dmr.openapi import OpenAPIConfig, build_schema, load_schema
7from dmr.openapi.objects import Components, PathItem, Tag
8from dmr.openapi.views import OpenAPIJsonView
9from dmr.routing import Router, external_path, path
10from examples.external_views.read_openapi import read_openapi_yaml
11
12
13@require_GET
14def number(request: HttpRequest, start: int, end: int) -> JsonResponse:
15 return JsonResponse(random.randint(start, end), safe=False)
16
17
18# Now, load the schema:
19raw_schema = read_openapi_yaml('openapi2.yml')
20
21# Create a router and URL patterns:
22router = Router(
23 'api/',
24 urls=[
25 external_path(
26 'number/<int:start>/<int:end>/',
27 number,
28 name='number',
29 openapi=load_schema(
30 raw_schema['paths']['/api/random/{start}/{end}'],
31 PathItem,
32 ),
33 ),
34 ],
35)
36
37# Register external components to the config:
38
39config = OpenAPIConfig(
40 title='New Random Number API',
41 version='0.0.1',
42 # Here you can pass any `OpenAPI` class parameters,
43 # not just components and tags:
44 components=load_schema(raw_schema['components'], Components),
45 tags=[load_schema(tag, Tag) for tag in raw_schema['tags']],
46)
47schema = build_schema(router, config=config)
48
49urlpatterns = [
50 # Register our router in the final url patterns:
51 router.to_urlpatterns(namespace='api'),
52 # Add swagger:
53 path('docs/openapi.json/', OpenAPIJsonView.as_view(schema), name='openapi'),
54]
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,
ValueErroris raisedWe 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:
1import random
2
3from django.http import HttpRequest, JsonResponse
4from django.views.decorators.http import require_GET
5
6from dmr.openapi import build_schema
7from dmr.openapi.views import OpenAPIJsonView
8from dmr.routing import Router, external_path, path
9
10
11@require_GET
12def number(request: HttpRequest) -> JsonResponse:
13 return JsonResponse(random.randint(1, 10), safe=False)
14
15
16# Create a router and URL patterns:
17router = Router(
18 'api/',
19 urls=[
20 # This function will still work, but be ignored from the spec:
21 external_path('number/', number, name='number', openapi=None),
22 ],
23)
24schema = build_schema(router)
25
26urlpatterns = [
27 # Register our router in the final url patterns:
28 router.to_urlpatterns(namespace='api'),
29 # Add swagger:
30 path('docs/openapi.json/', OpenAPIJsonView.as_view(schema), name='openapi'),
31]
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-allauthheadless views that are built to be used by the pure Django frameworkAny
APIVieworGenericAPIViewobjects fromdjango-rest-frameworkAny
ControllerBaseobjects fromdjango-ninja-extraAnd many more!