OpenAPI¶
We support OpenAPI versions from 3.0.0 through 3.2.0.
Note
By default, we use OpenAPI 3.1.0, since tooling such as Swagger, Scalar,
Redoc, and Stoplight does not yet fully support the latest specification.
You can track the current progress here.
Setting up OpenAPI views¶
We support:
Swagger with
SwaggerViewScalar with
ScalarViewopenapi.jsonwithOpenAPIJsonViewopenapi.yamlwithOpenAPIYamlViewwhen[openapi]extra is installed
Important
We recommend installing 'django-modern-rest[openapi]' when working with
OpenAPI. It enables schema validation, adds
OpenAPIYamlView, and supports
automatic example generation.
Here’s how it works:
1from django.urls import include
2
3from dmr.openapi import build_schema
4from dmr.openapi.views import (
5 OpenAPIJsonView,
6 RedocView,
7 ScalarView,
8 StoplightView,
9 SwaggerView,
10)
11from dmr.openapi.views.yaml import OpenAPIYamlView
12from dmr.routing import Router, path
13from examples.getting_started.msgspec_controller import UserController
14
15router = Router(
16 'api/',
17 [
18 path('user/', UserController.as_view(), name='users'),
19 ],
20)
21
22# Build the schema once and reuse it across all docs views.
23schema = build_schema(router)
24
25urlpatterns = [
26 # Mount the actual API endpoints.
27 path(router.prefix, include((router.urls, 'your_app'), namespace='api')),
28 # Machine-readable schema outputs for tooling and client generation.
29 path(
30 'docs/openapi.json/',
31 OpenAPIJsonView.as_view(schema),
32 name='openapi_json',
33 ),
34 path( # Requires the `django-modern-rest[openapi]` extra.
35 'docs/openapi.yaml/',
36 OpenAPIYamlView.as_view(schema),
37 name='openapi_yaml',
38 ),
39 # Human-friendly documentation UIs backed by the same schema.
40 path('docs/stoplight/', StoplightView.as_view(schema), name='stoplight'),
41 path('docs/swagger/', SwaggerView.as_view(schema), name='swagger'),
42 path('docs/scalar/', ScalarView.as_view(schema), name='scalar'),
43 path('docs/redoc/', RedocView.as_view(schema), name='redoc'),
44]
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"
},
"UserCreateModel": {
"properties": {
"email": {
"type": "string"
}
},
"required": [
"email"
],
"title": "UserCreateModel",
"type": "object"
},
"UserModel": {
"properties": {
"email": {
"type": "string"
},
"uid": {
"format": "uuid",
"type": "string"
}
},
"required": [
"email",
"uid"
],
"title": "UserModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/user/": {
"post": {
"deprecated": false,
"operationId": "postUsercontrollerApiUser",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserCreateModel"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserModel"
}
}
},
"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"
}
}
}
}
}
}
And then visit https://localhost:8000/docs/swagger/ (or any other renderer) for the interactive docs.
What happens in the example above?
We create / take an existing API
dmr.routing.Routerinstance and create an OpenAPI schema from it usingbuild_schema()Next, we define regular Django views that will serve you the API renderers
You can modify these views to
require auth / role / permissions / etcas all other regular Django views
Requirements for OpenAPI UIs¶
The HTML OpenAPI renderers
(SwaggerView,
RedocView,
ScalarView, and
StoplightView)
depend on both Django templates and static files.
To use the bundled UI pages:
Add
'dmr'toINSTALLED_APPS, so Django can discover the bundled renderer templatesIf you serve bundled assets locally, add
'django.contrib.staticfiles'toINSTALLED_APPSConfigure Django templates so app templates can be discovered, for example by enabling APP_DIRS in the Django template backend
Set STATIC_URL so Django can generate URLs for bundled static assets
In development, this is usually enough when using Django’s development server.
In production, make sure your static files setup is correct as described in the Django static files documentation and the staticfiles app reference.
If you switch renderers to CDN assets via
dmr.settings.Settings.openapi_static_cdn,
local static file serving is no longer required for those assets,
but adding 'dmr' to the list of installed apps and template
discovery are still required.
Note
By default, Swagger, Redoc, Stoplight, and Scalar use bundled static assets
shipped with django-modern-rest and served by Django.
To switch any renderer to a CDN, configure
dmr.settings.Settings.openapi_static_cdn.
Only renderers listed in that mapping will use CDN;
all others keep using local static files.
Exact bundled versions and license texts are documented in licenses/.
You can also modify the exact versions that we use for each tool this way.
Example:
>>> from dmr.settings import Settings
>>> DMR_SETTINGS = {
... Settings.openapi_static_cdn: {
... # or `@5.32.1`, or whatever other version:
... 'swagger': 'https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.32.0',
... },
... }
Choosing a renderer and CSP¶
For the general Content-Security-Policy setup with Django, see
Content Security Policy (CSP).
For OpenAPI specifically, the main thing to keep in mind is that final CSP compatibility still depends on the upstream renderer bundle you choose.
In general:
SwaggerViewis usually the best default when you want interactive docs with “try it out” support.RedocViewis a good fit for mostly read-only, reference-style documentation.ScalarViewandStoplightVieware worth considering when you prefer their UI, but they tend to be more opinionated frontends with more moving parts.
Known caveats:
If you switch to CDN assets, your CSP must allow those remote origins too.
In practice, Swagger and Redoc are usually easier starting points than more feature-heavy frontend bundles.
Exporting the schema¶
Note
To use this feature, you must add 'dmr' to INSTALLED_APPS in your
Django settings.
You can export the OpenAPI schema to stdout using the dmr_export_schema
management command. This is useful for sharing the schema, committing it to
version control, or automating client generation.
# Default JSON output:
python manage.py dmr_export_schema myapp.urls:schema
# Pretty-printed and sorted:
python manage.py dmr_export_schema myapp.urls:schema --indent 2 --sort-keys
# YAML output (requires 'django-modern-rest[openapi]' extra):
python manage.py dmr_export_schema myapp.urls:schema --format yaml --indent 2 --sort-keys
The positional argument is the import path to your
OpenAPI instance,
using a colon to separate the module from the attribute name
(e.g. myapp.urls:schema).
Available options:
--format—json(default) oryaml--indent— number of spaces, default:2--sort-keys— sort keys alphabetically in the output--no-ensure-ascii— do not quote all non-ascii chars
Customizing OpenAPI config¶
We support customizing dmr.openapi.OpenAPIConfig
that will be used for the final schema in two ways:
By defining
dmr.settings.Settings.openapi_configsetting insideDMR_SETTINGSin yoursettings.pyBy passing
OpenAPIConfiginstance intobuild_schema()
For example, this is how you can change some OpenAPI metadata, including the spec version:
1from django.urls import include
2
3from dmr.openapi import OpenAPIConfig, build_schema
4from dmr.openapi.objects import Server
5from dmr.openapi.views import (
6 OpenAPIJsonView,
7 RedocView,
8 ScalarView,
9 StoplightView,
10 SwaggerView,
11)
12from dmr.routing import Router, path
13from examples.getting_started.msgspec_controller import UserController
14
15router = Router(
16 'api/',
17 [
18 path('user/', UserController.as_view(), name='users'),
19 ],
20)
21
22config = OpenAPIConfig(
23 title='My awesome API',
24 version='1.0.0',
25 openapi_version='3.2.0',
26 servers=[
27 Server(url='https://prod.example.com'),
28 Server(url='https://dev.example.com'),
29 ],
30)
31schema = build_schema(router, config=config)
32
33urlpatterns = [
34 path(router.prefix, include((router.urls, 'your_app'), namespace='api')),
35 path('docs/openapi.json/', OpenAPIJsonView.as_view(schema), name='openapi'),
36 path('docs/swagger/', SwaggerView.as_view(schema), name='swagger'),
37 path('docs/scalar/', ScalarView.as_view(schema), name='scalar'),
38 path('docs/redoc/', RedocView.as_view(schema), name='redoc'),
39 path('docs/stoplight/', StoplightView.as_view(schema), name='stoplight'),
40]
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"
},
"UserCreateModel": {
"properties": {
"email": {
"type": "string"
}
},
"required": [
"email"
],
"title": "UserCreateModel",
"type": "object"
},
"UserModel": {
"properties": {
"email": {
"type": "string"
},
"uid": {
"format": "uuid",
"type": "string"
}
},
"required": [
"email",
"uid"
],
"title": "UserModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "My awesome API",
"version": "1.0.0"
},
"openapi": "3.2.0",
"paths": {
"/api/user/": {
"post": {
"deprecated": false,
"operationId": "postUsercontrollerApiUser",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserCreateModel"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserModel"
}
}
},
"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"
}
}
}
}
},
"servers": [
{
"url": "https://prod.example.com"
},
{
"url": "https://dev.example.com"
}
]
}
Customizing OpenAPI generation¶
Customizing schema¶
We delegate all schema generation to the model’s library directly.
To do so, we use BaseSchemaGenerator
subclasses for different serializers.
To customize a schema, use the native methods.
Docs: https://docs.pydantic.dev/latest/concepts/json_schema
1import pydantic
2
3
4class UserModel(pydantic.BaseModel):
5 email: str = pydantic.Field(
6 json_schema_extra={'example': 'user@example.com'},
7 )
Common features:
You can completely redefine the schema generation with providing
pydantic.json_schema.WithJsonSchemaannotation or by overriding__get_pydantic_json_schema__method on a pydantic modelYou can change the
titleof generics pydantic models by redefiningpydantic.BaseModel.model_parametrized_name()
Note
By default docstring or __doc__ from the model is used as a description.
Customizing path items¶
Controller allows customizing some metadata
for PathItem:
1from dmr import Controller
2from dmr.openapi.objects import Server
3from dmr.plugins.msgspec import MsgspecSerializer
4
5
6class UserController(Controller[MsgspecSerializer]):
7 description = 'Create new users'
8 servers = (
9 Server(url='https://example.com'),
10 Server(url='https://dev.example.com'),
11 )
12
13 def post(self) -> str:
14 return 'post'
15
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"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usercontroller/": {
"description": "Create new users",
"post": {
"deprecated": false,
"operationId": "postUsercontrollerApiUsercontroller",
"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"
}
}
},
"servers": [
{
"url": "https://example.com"
},
{
"url": "https://dev.example.com"
}
]
}
}
}
Note
By default docstring or __doc__ from the controller
is used to generate summary and description
for the PathItem.
Customizing operation¶
@modify and @validate
can be used to customize the resulting Operation
metadata.
1from http import HTTPStatus
2
3from django.http import HttpResponse
4
5from dmr import Controller, ResponseSpec, modify, validate
6from dmr.openapi.objects import Server
7from dmr.plugins.msgspec import MsgspecSerializer
8
9
10class UserController(Controller[MsgspecSerializer]):
11 @modify(servers=[Server(url='https://example.com')])
12 def post(self) -> str:
13 return 'post'
14
15 @validate(
16 ResponseSpec(status_code=HTTPStatus.OK, return_type=str),
17 description='PUT operation description',
18 tags=['Public'],
19 )
20 def put(self) -> HttpResponse:
21 return self.to_response('put')
22
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"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usercontroller/": {
"post": {
"deprecated": false,
"operationId": "postUsercontrollerApiUsercontroller",
"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"
}
},
"servers": [
{
"url": "https://example.com"
}
]
},
"put": {
"deprecated": false,
"description": "PUT operation description",
"operationId": "putUsercontrollerApiUsercontroller",
"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"
}
},
"tags": [
"Public"
]
}
}
}
}
Note
By default docstring or __doc__ from endpoint’s function definition
is used to generate summary and description
for the Operation.
Customizing router-level metadata¶
Router supports tags and deprecated parameters
to apply OpenAPI metadata to all operations in the router:
1from django.urls import include
2
3from dmr.openapi import build_schema
4from dmr.openapi.views import OpenAPIJsonView
5from dmr.routing import Router, path
6from examples.getting_started.msgspec_controller import UserController
7
8router = Router(
9 'api/v1/users/',
10 [
11 path('', UserController.as_view()),
12 path('<int:user_id>/', UserController.as_view()),
13 ],
14 tags=['users'], # All endpoints tagged as 'users'
15 deprecated=True, # All endpoints are deprecated
16)
17schema = build_schema(router)
18
19urlpatterns = [
20 # Register our router in the final url patterns:
21 path(router.prefix, include((router.urls, 'test_app'), namespace='api')),
22 # Add swagger:
23 path('docs/openapi.json/', OpenAPIJsonView.as_view(schema), name='openapi'),
24]
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"
},
"UserCreateModel": {
"properties": {
"email": {
"type": "string"
}
},
"required": [
"email"
],
"title": "UserCreateModel",
"type": "object"
},
"UserModel": {
"properties": {
"email": {
"type": "string"
},
"uid": {
"format": "uuid",
"type": "string"
}
},
"required": [
"email",
"uid"
],
"title": "UserModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/v1/users/": {
"post": {
"deprecated": true,
"operationId": "postUsercontrollerApiV1Users",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserCreateModel"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserModel"
}
}
},
"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"
}
},
"tags": [
"users"
]
}
},
"/api/v1/users/{user_id}/": {
"post": {
"deprecated": true,
"operationId": "postUsercontrollerApiV1UsersUserId",
"parameters": [
{
"deprecated": false,
"in": "path",
"name": "user_id",
"required": true,
"schema": {
"type": "integer"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserCreateModel"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserModel"
}
}
},
"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"
}
},
"tags": [
"users"
]
}
}
}
}
tags: List of strings to group operations in OpenAPI documentationdeprecated: Boolean flag to mark all operations in this router as deprecated
These router-level settings are automatically merged with endpoint-level customizations
set via @modify or @validate.
Router tags are prepended to endpoint tags, and deprecated is set to True
if either the router or endpoint has it enabled.
You can also set tags and deprecated at the individual endpoint level
via @modify to override or extend router-level settings.
Customizing parameter¶
There are different styles and other features
that Parameter supports
in OpenAPI Parameters.
For example, if you want to change how Query
parameter is documented with the help
of dmr.openapi.objects.ParameterMetadata annotation:
1from typing import Annotated
2
3import msgspec
4
5from dmr import Controller, Query
6from dmr.openapi.objects import ParameterMetadata
7from dmr.plugins.msgspec import MsgspecSerializer
8
9
10class QueryModel(msgspec.Struct):
11 search: str
12 max_items: int
13
14
15class UserController(
16 Controller[MsgspecSerializer],
17):
18 def post(
19 self,
20 parsed_query: Query[
21 Annotated[
22 QueryModel,
23 ParameterMetadata(
24 description='Old way to search things',
25 deprecated=True,
26 ),
27 ]
28 ],
29 ) -> str:
30 return 'post'
31
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"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usercontroller/": {
"post": {
"deprecated": false,
"operationId": "postUsercontrollerApiUsercontroller",
"parameters": [
{
"deprecated": true,
"description": "Old way to search things",
"in": "query",
"name": "search",
"required": true,
"schema": {
"type": "string"
}
},
{
"deprecated": true,
"description": "Old way to search things",
"in": "query",
"name": "max_items",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"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"
}
}
}
}
}
}
Customizing media types¶
There are different metadata fields, like examples and encoding,
that MediaType supports
in OpenAPI MediaType.
For example, if you want to change how Body
provides examples,
you can use dmr.openapi.objects.MediaTypeMetadata annotation:
1from typing import Annotated
2
3import pydantic
4
5from dmr import Body, Controller
6from dmr.openapi.objects import MediaTypeMetadata
7from dmr.plugins.pydantic import PydanticSerializer
8
9
10class SearchModel(pydantic.BaseModel):
11 search: str
12 max_items: int
13
14
15example = SearchModel(search='example', max_items=10).model_dump(mode='json')
16
17
18class UserController(
19 Controller[PydanticSerializer],
20):
21 def post(
22 self,
23 parsed_body: Body[
24 Annotated[
25 SearchModel,
26 MediaTypeMetadata(
27 example=example,
28 ),
29 ]
30 ],
31 ) -> str:
32 return 'post'
33
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"
},
"SearchModel": {
"properties": {
"max_items": {
"title": "Max Items",
"type": "integer"
},
"search": {
"title": "Search",
"type": "string"
}
},
"required": [
"search",
"max_items"
],
"title": "SearchModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usercontroller/": {
"post": {
"deprecated": false,
"operationId": "postUsercontrollerApiUsercontroller",
"requestBody": {
"content": {
"application/json": {
"example": {
"max_items": 10,
"search": "example"
},
"schema": {
"$ref": "#/components/schemas/SearchModel"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"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"
}
}
}
}
}
}
We also support the same way for conditional types:
1from typing import Annotated, TypeAlias
2
3import pydantic
4
5from dmr import Body, Controller
6from dmr.negotiation import ContentType, conditional_type
7from dmr.openapi.objects import MediaTypeMetadata
8from dmr.plugins.msgspec import MsgspecJsonParser
9from dmr.plugins.pydantic import PydanticSerializer
10from examples.negotiation.negotiation import XmlParser
11
12
13class _SearchModel(pydantic.BaseModel):
14 search: str
15 max_items: int
16
17
18class XmlSearchModel(pydantic.BaseModel):
19 search: str
20
21
22SearchModel: TypeAlias = Annotated[
23 _SearchModel,
24 MediaTypeMetadata(
25 example=_SearchModel(search='example', max_items=10).model_dump(
26 mode='json',
27 ),
28 ),
29]
30
31
32class UserController(
33 Controller[PydanticSerializer],
34):
35 parsers = (MsgspecJsonParser(), XmlParser())
36
37 def post(
38 self,
39 parsed_body: Body[
40 Annotated[
41 _SearchModel | XmlSearchModel,
42 conditional_type({
43 ContentType.json: SearchModel,
44 ContentType.xml: XmlSearchModel,
45 }),
46 ],
47 ],
48 ) -> str:
49 return 'post'
50
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"
},
"XmlSearchModel": {
"properties": {
"search": {
"title": "Search",
"type": "string"
}
},
"required": [
"search"
],
"title": "XmlSearchModel",
"type": "object"
},
"_SearchModel": {
"properties": {
"max_items": {
"title": "Max Items",
"type": "integer"
},
"search": {
"title": "Search",
"type": "string"
}
},
"required": [
"search",
"max_items"
],
"title": "_SearchModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usercontroller/": {
"post": {
"deprecated": false,
"operationId": "postUsercontrollerApiUsercontroller",
"requestBody": {
"content": {
"application/json": {
"example": {
"max_items": 10,
"search": "example"
},
"schema": {
"$ref": "#/components/schemas/_SearchModel"
}
},
"application/xml": {
"schema": {
"$ref": "#/components/schemas/XmlSearchModel"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"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"
}
}
}
}
}
}
And for FileMetadata:
1from typing import Annotated
2
3import pydantic
4
5from dmr import Controller, FileMetadata
6from dmr.openapi.objects import Encoding, MediaTypeMetadata
7from dmr.parsers import MultiPartParser
8from dmr.plugins.pydantic import PydanticSerializer
9
10
11class FileModel(pydantic.BaseModel):
12 size: int = pydantic.Field(le=1024 * 5)
13
14
15class UserUpload(pydantic.BaseModel):
16 avatar: FileModel
17
18
19class UserController(
20 Controller[PydanticSerializer],
21):
22 parsers = (MultiPartParser(),)
23
24 def post(
25 self,
26 parsed_file_metadata: FileMetadata[
27 Annotated[
28 UserUpload,
29 MediaTypeMetadata(
30 # Note, that this can also inferred from `Literal` type
31 # in `FileModel.content_type` property, but can be set here:
32 encoding={'avatar': Encoding(content_type='image/png')},
33 ),
34 ]
35 ],
36 ) -> str:
37 return 'post'
38
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/usercontroller/": {
"post": {
"deprecated": false,
"operationId": "postUsercontrollerApiUsercontroller",
"requestBody": {
"content": {
"multipart/form-data": {
"encoding": {
"avatar": {
"contentType": "image/png"
}
},
"schema": {
"properties": {
"avatar": {
"format": "binary",
"type": "string"
}
},
"required": [
"avatar"
],
"title": "UserUpload",
"type": "object"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"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"
}
}
}
}
}
}
Customizing response¶
ResponseSpec supports all the metadata fields
that Response has.
Providing an explicit Link for schemathesis
stateful API testing
would look like so:
1import uuid
2from http import HTTPStatus
3
4import msgspec
5from django.http import HttpResponse
6
7from dmr import Controller, ResponseSpec, modify, validate
8from dmr.openapi.objects import Link
9from dmr.plugins.msgspec import MsgspecSerializer
10
11
12class UserModel(msgspec.Struct):
13 uid: uuid.UUID
14
15
16class UserController(Controller[MsgspecSerializer]):
17 @modify(
18 response_description='This is a description for your response',
19 links={
20 'GetUser': Link(
21 operation_id='getUser',
22 parameters={'userId': '$response.body#/uid'},
23 ),
24 },
25 )
26 def post(self) -> UserModel:
27 return UserModel(uid=uuid.uuid4())
28
29 @validate(
30 ResponseSpec(
31 status_code=HTTPStatus.OK,
32 return_type=UserModel,
33 links={
34 'GetUser': Link(
35 operation_id='getUser',
36 parameters={'userId': '$response.body#/uid'},
37 ),
38 },
39 ),
40 )
41 def put(self) -> HttpResponse:
42 return self.to_response(UserModel(uid=uuid.uuid4()))
43
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": {
"uid": {
"format": "uuid",
"type": "string"
}
},
"required": [
"uid"
],
"title": "UserModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usercontroller/": {
"post": {
"deprecated": false,
"operationId": "postUsercontrollerApiUsercontroller",
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserModel"
}
}
},
"description": "This is a description for your response",
"links": {
"GetUser": {
"operationId": "getUser",
"parameters": {
"userId": "$response.body#/uid"
}
}
}
},
"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"
}
}
},
"put": {
"deprecated": false,
"operationId": "putUsercontrollerApiUsercontroller",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserModel"
}
}
},
"description": "OK",
"links": {
"GetUser": {
"operationId": "getUser",
"parameters": {
"userId": "$response.body#/uid"
}
}
}
},
"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"
}
}
}
}
}
}
Examples generation¶
If you installed 'django-modern-rest[openapi]' extra
and enabled openapi_examples_seed setting,
we will generate missing examples in your OpenAPI schemas using
polyfactory.
They will not have the best data quality, since they are clearly autogenerated from fake data, but sometimes it is better than nothing.
1import uuid
2
3import msgspec
4
5from dmr import Controller, Query
6from dmr.plugins.msgspec import MsgspecSerializer
7
8
9class QueryModel(msgspec.Struct):
10 search: str
11 max_items: int
12
13
14class UserModel(msgspec.Struct):
15 uid: uuid.UUID
16 username: str
17
18
19class UserController(
20 Controller[MsgspecSerializer],
21):
22 def post(self, parsed_query: Query[QueryModel]) -> UserModel:
23 return UserModel(uid=uuid.uuid4(), username='example')
24
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.",
"example": {
"detail": [
{
"loc": [
"vbzQIBShDtFfOfiPXwvm"
],
"msg": "gNZYFcagWptUqCwdERil"
}
]
},
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
},
"UserModel": {
"example": {
"uid": "7b89296c-6dcb-4c50-8857-7eb1924770d3",
"username": "EkQQHiBrmXZcSFtoJxJI"
},
"properties": {
"uid": {
"format": "uuid",
"type": "string"
},
"username": {
"type": "string"
}
},
"required": [
"uid",
"username"
],
"title": "UserModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usercontroller/": {
"post": {
"deprecated": false,
"operationId": "postUsercontrollerApiUsercontroller",
"parameters": [
{
"deprecated": false,
"in": "query",
"name": "search",
"required": true,
"schema": {
"type": "string"
}
},
{
"deprecated": false,
"in": "query",
"name": "max_items",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserModel"
}
}
},
"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"
}
}
}
}
}
}
Important
However, we recommend adding semantic named examples by hand.
Top level API¶
This is how OpenAPI spec is generated, top level overview:
---
config:
theme: forest
---
graph
Start[build_schema] --> Router[Router];
Router -->|for each controller| Controller[Controller.get_path_item];
Router -->|for each defined auth| SecurityScheme[Auth.security_scheme];
Controller -->|for each endpoint| Endpoint[Endpoint.get_schema];
Endpoint -->|for each component| ComponentParser[ComponentParser.get_schema]
Endpoint -->|for each response| ResponseSpec[ResponseSpec.get_schema];
Endpoint -->|for each used auth| SecurityRequirement[Auth.security_requirement];
ComponentParser -->|for each schema| Schema[serializer.schema_generator.get_schema];
ResponseSpec -->|for each schema| Schema[serializer.schema_generator.get_schema];
OpenAPI spec generation¶
We have several major design principles that define our API:
Regular user-facing objects must know how to build the OpenAPI schema. For example:
Endpoint,Controller, andRouterall know how to build the spec for themselves. Since they are user-facing, it is easy to modify how the schema generation works if neededAll model schemas must be directly generated by their libraries. We don’t do anything with the JSON Schema that is generated by
pydanticormsgspec. They can do a better job than we do. However, their schemas still can be customized. SeePydanticSchemaGeneratorandMsgspecSchemaGenerator
APIs for schema overrides¶
Useful APIs for users to override:
dmr.openapi.build_schema()to change howOpenAPIConfigandOpenAPIContextare generateddmr.routing.Router.get_schema()to change howOpenAPIandComponentsare generateddmr.controller.Controller.get_path_item()to change howPathItemobjects are generateddmr.endpoint.Endpoint.get_schema()to change howOperationis generateddmr.components.ComponentParser.get_schema()to change howParameterobjects are generateddmr.metadata.ResponseSpec.get_schema()to change howResponseobjects are generateddmr.security.SyncAuth.security_schemes()anddmr.security.SyncAuth.security_requirementto change howSecuritySchemeand requirements are generated
API Reference¶
This is the API every user needs:
- dmr.openapi.build_schema(router: Router, *, context: OpenAPIContext) OpenAPI[source]¶
- dmr.openapi.build_schema(router: Router, *, config: OpenAPIConfig | None = None) OpenAPI
Build OpenAPI schema.
- Parameters:
router – Router that contains all API endpoints and all controllers.
context – OpenAPI context with all the builder tools.
config – Optional configuration of OpenAPI metadata. Can be
None, in this case we fetch OpenAPI config from settings.
- class dmr.openapi.OpenAPIConfig(*, title: str, version: str, openapi_version: Literal['3.0.0', '3.1.0', '3.2.0'] = '3.1.0', summary: str | None = None, description: str | None = None, terms_of_service: str | None = None, contact: Contact | None = None, external_docs: ExternalDocumentation | None = None, security: list[dict[str, list[str]]] | None = None, license: License | None = None, components: Components | list[Components] | None = None, servers: list[Server] | None = None, tags: list[Tag] | None = None, webhooks: dict[str, PathItem | Reference] | None = None)[source]¶
Configuration class for customizing OpenAPI specification metadata.
This class provides a way to configure various aspects of the OpenAPI specification that will be generated for your API documentation. It allows you to customize the API information, contact details, licensing, security requirements, and other metadata that appears in the generated OpenAPI spec.
- class dmr.openapi.OpenAPIContext(config: OpenAPIConfig)[source]¶
Context for OpenAPI specification generation.
Maintains shared state and generators used across the OpenAPI generation process. Provides access to different generators.
- register_schema(annotation: Any, schema: Schema | Reference | SchemaCallback, *, override: bool = False) None[source]¶
Register top-level annotation resolution into an OpenAPI schema.
You can pass either a schema object itself, a reference, or a callback that returns schema, reference, or
Noneto fallback to the default schema resolution process.Warning
This only works for the top-level annotations with direct matches. For example: when you register
Userto have a specific schema, it will take effect only in cases whereUseris used directly.list[User]will use the default serializer schema resolution strategy.
All other objects that are only used if you decide to customize the schema are listed in OpenAPI Reference.