OpenAPI#
We support OpenAPI versions from 3.1.0 through 3.2.0,
including every object and field that these specifications define.
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.
Important
OpenAPI 3.0.x is not supported. It predates JSON Schema,
while we generate all model schemas as JSON Schema with pydantic
or msgspec. Passing it to dmr.openapi.OpenAPIConfig
raises a ValueError.
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 dmr.openapi import build_schema
2from dmr.openapi.views import (
3 OpenAPIJsonView,
4 RedocView,
5 ScalarView,
6 StoplightView,
7 SwaggerView,
8)
9from dmr.openapi.views.yaml import OpenAPIYamlView
10from dmr.routing import Router, path
11from examples.getting_started.msgspec_controller import UserController
12
13router = Router(
14 'api/',
15 [
16 path('user/', UserController.as_view(), name='users'),
17 ],
18)
19
20# Build the schema once and reuse it across all docs views.
21schema = build_schema(router)
22
23urlpatterns = [
24 # Mount the actual API endpoints.
25 router.to_urlpatterns(namespace='api'),
26 # Machine-readable schema outputs for tooling and client generation.
27 path(
28 'docs/openapi.json/',
29 OpenAPIJsonView.as_view(schema),
30 name='openapi_json',
31 ),
32 path( # Requires the `django-modern-rest[openapi]` extra.
33 'docs/openapi.yaml/',
34 OpenAPIYamlView.as_view(schema),
35 name='openapi_yaml',
36 ),
37 # Human-friendly documentation UIs backed by the same schema.
38 path('docs/stoplight/', StoplightView.as_view(schema), name='stoplight'),
39 path('docs/swagger/', SwaggerView.as_view(schema), name='swagger'),
40 path('docs/scalar/', ScalarView.as_view(schema), name='scalar'),
41 path('docs/redoc/', RedocView.as_view(schema), name='redoc'),
42]
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"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/user/": {
"post": {
"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 http://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
Caching schema responses#
The schema conversion result is cached after the first conversion.
For public JSON/YAML schema endpoints, you can also cache the serialized response with Django’s cache_page decorator in URLconf:
1from typing import Final
2
3from django.views.decorators.cache import cache_page
4
5from dmr.openapi import build_schema
6from dmr.openapi.views import OpenAPIJsonView
7from dmr.openapi.views.yaml import OpenAPIYamlView
8from dmr.routing import Router, path
9from examples.getting_started.msgspec_controller import UserController
10
11_CACHE_TIMEOUT: Final = 900 # 15 minutes
12
13
14router = Router(
15 'api/',
16 [
17 path('user/', UserController.as_view(), name='users'),
18 ],
19)
20schema = build_schema(router)
21
22urlpatterns = [
23 router.to_urlpatterns(namespace='api'),
24 path(
25 'docs/openapi.json/',
26 cache_page(_CACHE_TIMEOUT)(OpenAPIJsonView.as_view(schema)),
27 name='openapi_json',
28 ),
29 path(
30 'docs/openapi.yaml/',
31 cache_page(_CACHE_TIMEOUT)(OpenAPIYamlView.as_view(schema)),
32 name='openapi_yaml',
33 ),
34]
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"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/user/": {
"post": {
"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"
}
}
}
}
}
}
Cache hits skip JSON/YAML serialization. This example uses the default
Django cache; configure its backend through
CACHES.
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 dmr.openapi import OpenAPIConfig, build_schema
2from dmr.openapi.objects import Server
3from dmr.openapi.views import OpenAPIJsonView, SwaggerView
4from dmr.routing import Router, path
5from examples.getting_started.msgspec_controller import UserController
6
7router = Router(
8 'api/',
9 [
10 path('user/', UserController.as_view(), name='users'),
11 ],
12)
13
14config = OpenAPIConfig(
15 title='My awesome API',
16 version='1.0.0',
17 openapi_version='3.2.0',
18 servers=[
19 Server(url='https://prod.example.com'),
20 Server(url='https://dev.example.com'),
21 ],
22)
23schema = build_schema(router, config=config)
24
25urlpatterns = [
26 router.to_urlpatterns(namespace='api'),
27 path('docs/openapi.json/', OpenAPIJsonView.as_view(schema), name='openapi'),
28 path('docs/swagger/', SwaggerView.as_view(schema), name='swagger'),
29]
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"
}
}
},
"info": {
"title": "My awesome API",
"version": "1.0.0"
},
"openapi": "3.2.0",
"paths": {
"/api/user/": {
"post": {
"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"
}
]
}
You can also pass components and other parts of the OpenAPI spec
from some other source, like pre-existing schemas.
To learn more, see External views guide.
Customizing OpenAPI context#
Added in version 0.16.0.
To replace some internal logic, subclass OpenAPIContext
and set the corresponding *_cls attribute to your subclass.
Pass an instance of your context to build_schema().
Configuration values stay in OpenAPIConfig;
behavioral customizations belong in the generator, registries,
or merger subclasses.
For example, this context generates operation IDs without controller names:
1from typing import ClassVar
2
3from typing_extensions import override
4
5from dmr import Controller
6from dmr.metadata import EndpointMetadata
7from dmr.openapi import OpenAPIConfig, OpenAPIContext, build_schema
8from dmr.openapi.generators import OperationIdGenerator
9from dmr.openapi.views import OpenAPIJsonView
10from dmr.routing import Router, path
11from dmr.serializer import BaseSerializer
12from examples.getting_started.msgspec_controller import UserController
13
14
15class PathOperationIdGenerator(OperationIdGenerator):
16 """Generate IDs from the HTTP method and path, without controller names."""
17
18 @override
19 def generate_suffix(
20 self,
21 path: str,
22 metadata: EndpointMetadata,
23 controller_cls: type[Controller[BaseSerializer]],
24 ) -> str:
25 # Keep explicit IDs and duplicate detection from the base generator:
26 return ''
27
28
29class CustomContext(OpenAPIContext):
30 operation_id_cls: ClassVar[type[OperationIdGenerator]] = (
31 PathOperationIdGenerator
32 )
33
34
35router = Router('api/', [path('user/', UserController.as_view())])
36config = OpenAPIConfig(title='My API', version='1.0.0')
37schema = build_schema(router, context=CustomContext(config))
38
39urlpatterns = [
40 router.to_urlpatterns(namespace='api'),
41 path('docs/openapi.json/', OpenAPIJsonView.as_view(schema), name='openapi'),
42]
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"
}
}
},
"info": {
"title": "My API",
"version": "1.0.0"
},
"openapi": "3.1.0",
"paths": {
"/api/user/": {
"post": {
"operationId": "postApiUser",
"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"
}
}
}
}
}
}
POST /api/user/ now has the operation ID postApiUser.
Calling the base generator with an empty controller-name argument preserves
explicit endpoint operation_id values and duplicate detection.
If you replace the generation logic entirely, your implementation must
handle explicit IDs and register the final ID with
self._context.registries.operation_id.register() to retain those guarantees.
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 schema generator
Added in version 0.16.0.
You can also change the native tools schema generation behavior.
To do so, subclass the plugin’s schema generator and create your own serializer that uses it.
msgspec allows passing extra keyword arguments
to msgspec.json.schema()
via json_schema_kwargs.
Note that ref_template and mode are always defined by us.
pydantic allows passing extra keyword arguments
to pydantic.TypeAdapter.json_schema()
via json_schema_kwargs.
Note that ref_template and mode are always defined by us.
Customizing path items#
Controller allows customizing some metadata
for PathItem:
1from dmr import Controller
2from dmr.openapi.objects import ExternalDocumentation, Server
3from dmr.plugins.msgspec import MsgspecSerializer
4
5
6class UserController(Controller[MsgspecSerializer]):
7 """
8 Users API.
9
10 Replaced description.
11 """ # This docstring becomes the `summary`, but not `description`
12
13 description = 'Create new users' # Set explicitly, not from the docstring
14 servers = (
15 Server(url='https://example.com'),
16 Server(url='https://dev.example.com'),
17 )
18 # These are used for all operations, unless an endpoint sets its own:
19 deprecated = True
20 external_docs = ExternalDocumentation(url='https://example.com/docs')
21
22 def post(self) -> str:
23 return 'post'
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"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usercontroller/": {
"description": "Create new users",
"post": {
"deprecated": true,
"externalDocs": {
"url": "https://example.com/docs"
},
"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"
}
]
},
"summary": "Users API."
}
}
}
Note
By default docstring or __doc__ from the controller
is used to generate summary and description
for the PathItem:
its first paragraph becomes the summary
and everything after it becomes the description.
Set summary
or description
to None to leave them out of the schema.
tags, security, servers, deprecated, external_docs,
and callbacks are operation fields,
but they can also be set on the controller level
as defaults for all its operations. As everywhere in django-modern-rest,
the first explicitly defined level wins: endpoint, then controller,
then router for tags and deprecated,
or OpenAPIConfig for security.
Set servers, external_docs, or callbacks to None
on an endpoint to remove the controller value from that operation.
Changed in version 0.16.0: Controller docstrings are now used as the default
summary and description of a path item.
Added deprecated, external_docs, and callbacks
controller attributes. servers is now dumped
on every operation instead of the path item.
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"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usercontroller/": {
"post": {
"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": {
"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,
by the same rules that a controller uses for its path item:
its first paragraph becomes the summary
and everything after it becomes the description.
Pass summary=None or description=None
to leave them out of the schema.
Changed in version 0.16.0: summary and description are now resolved one at a time.
Passing only one of them used to drop the docstring entirely.
Customizing security#
Added in version 0.16.0.
Regular authentication is documented automatically:
each dmr.security.SyncAuth and dmr.security.AsyncAuth
instance from auth contributes its
SecurityScheme objects —
possibly none, possibly several —
and needed security requirements for the operation.
Some security mechanisms, however, are not part of your Django app at all.
For example, an API gateway that checks a header before the request
reaches you, or mTLS between the services in your cluster.
Use security to document them, it accepts any sequence of
SecurityRequirement objects.
Warning
security only documents a mechanism in the schema,
it is never enforced at runtime. Use auth to actually
protect your endpoints, see How authentication works.
1from dmr import Controller, modify
2from dmr.openapi import OpenAPIConfig, build_schema
3from dmr.openapi.objects import Components, SecurityScheme
4from dmr.openapi.views import OpenAPIJsonView
5from dmr.plugins.msgspec import MsgspecSerializer
6from dmr.routing import Router, path
7from dmr.security.django_session import DjangoSessionSyncAuth
8
9
10class UserController(Controller[MsgspecSerializer]):
11 # Requests can also arrive through the API gateway,
12 # it is an alternative to the auth of each endpoint:
13 security = ({'gateway': []},)
14
15 @modify(auth=[DjangoSessionSyncAuth()])
16 def get(self) -> str:
17 return 'get'
18
19 # This endpoint is only called by other services in the mesh,
20 # it overrides the controller-level requirement:
21 @modify(security=[{'mesh': []}])
22 def post(self) -> str: # NOTE: runtime has no auth! Only spec!
23 return 'post'
24
25
26router = Router('api/', [path('user/', UserController.as_view())])
27
28config = OpenAPIConfig(
29 title='My awesome API',
30 version='1.0.0',
31 # Schemes used in `security` must be declared by hand:
32 components=Components(
33 security_schemes={
34 'gateway': SecurityScheme(
35 type='apiKey',
36 name='X-Gateway-Key',
37 security_scheme_in='header',
38 ),
39 'mesh': SecurityScheme(
40 type='mutualTLS',
41 description='Service mesh mTLS',
42 ),
43 },
44 ),
45)
46schema = build_schema(router, config=config)
47
48urlpatterns = [
49 router.to_urlpatterns(namespace='api'),
50 path('docs/openapi.json/', OpenAPIJsonView.as_view(schema), name='openapi'),
51]
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": {
"csrf": {
"description": "CSRF protection",
"in": "cookie",
"name": "csrftoken",
"type": "apiKey"
},
"django_session": {
"description": "Reusing standard Django auth flow for API",
"in": "cookie",
"name": "sessionid",
"type": "apiKey"
},
"gateway": {
"in": "header",
"name": "X-Gateway-Key",
"type": "apiKey"
},
"mesh": {
"description": "Service mesh mTLS",
"type": "mutualTLS"
}
}
},
"info": {
"title": "My awesome API",
"version": "1.0.0"
},
"openapi": "3.1.0",
"paths": {
"/api/user/": {
"get": {
"operationId": "getUserControllerApiUser",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "OK"
},
"401": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when auth was not successful"
},
"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"
}
},
"security": [
{
"django_session": []
},
{
"gateway": []
}
]
},
"post": {
"operationId": "postUserControllerApiUser",
"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"
}
},
"security": [
{
"mesh": []
}
]
}
}
}
}
Security requirements can be defined on three levels,
just like auth is defined:
In settings with
securityofOpenAPIConfig, it applies to all endpoints of your app and it is also used as the document-levelsecurityOn a controller with the
securityattribute, it applies to all endpoints of this controller
These rules define how the final list is built:
Levels are not merged together, the first explicitly defined level wins: endpoint, then controller, then settings. This is how all other metadata is resolved as well
Setting
security=Noneorsecurity=[]on an endpoint or a controller disables all less specific levels, exactly likeauth=Nonedoes. Withoutauthsuch an operation getssecurity: [], which opts out of the document-level requirements instead of inheriting themThe resolved requirements are merged with the requirements generated from
authbysecurity_merger. By default, they are added as-is after theauthones, each requirement is an alternative: a client can satisfy any of themThe default merger raises
EndpointMetadataErrorduring the schema generation when the same requirement is present twice, for example, whensecurityrepeats whatauthalready generatesSecurity schemes used in
securityare not registered automatically, declare them incomponentsofOpenAPIConfig. Schemes thatauthregisters can be reused insecurity
To change how auth and security are merged, for example,
to require both of them at once with AND logic,
subclass SecurityRequirementMerger
and set its instance as security_merger of your
SecuritySchemeGenerator subclass.
Then use this generator in your OpenAPI context,
see Customizing OpenAPI context.
Note
Do not use security to change the schema that was generated by auth.
Subclass the auth class you need and override
dmr.security.SyncAuth.security_schemes()
or dmr.security.SyncAuth.security_requirements() instead.
Danger
Setting security only modifies the OpenAPI spec,
not the runtime enforcing of the security.
Only use it to describe the schema.
Prefer auth whenever possible.
Customizing router-level metadata#
Router supports tags and deprecated parameters
to apply OpenAPI metadata to all operations in the router:
1from dmr.openapi import build_schema
2from dmr.openapi.views import OpenAPIJsonView
3from dmr.routing import Router, path
4from examples.getting_started.msgspec_controller import UserController
5
6router = Router(
7 'api/v1/users/',
8 [
9 path('', UserController.as_view()),
10 path('<int:user_id>/', UserController.as_view()),
11 ],
12 tags=['users'], # All endpoints tagged as 'users'
13 deprecated=True, # All endpoints are deprecated
14)
15schema = build_schema(router)
16
17urlpatterns = [
18 # Register our router in the final url patterns:
19 router.to_urlpatterns(namespace='api'),
20 # Add swagger:
21 path('docs/openapi.json/', OpenAPIJsonView.as_view(schema), name='openapi'),
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"
},
"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"
}
}
},
"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": [
{
"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
Router tags are used for operations without controller-level
or endpoint-level 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 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(Controller[MsgspecSerializer]):
16 def post(
17 self,
18 parsed_query: Query[
19 Annotated[
20 QueryModel,
21 ParameterMetadata(
22 description='Old way to search things',
23 deprecated=True,
24 ),
25 ]
26 ],
27 ) -> str:
28 return 'post'
29
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"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usercontroller/": {
"post": {
"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 PydanticFastSerializer
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(Controller[PydanticFastSerializer]):
19 def post(
20 self,
21 parsed_body: Body[
22 Annotated[
23 SearchModel,
24 MediaTypeMetadata(
25 example=example,
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"
}
]
},
"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"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usercontroller/": {
"post": {
"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(Controller[PydanticSerializer]):
33 parsers = (MsgspecJsonParser(), XmlParser())
34
35 def post(
36 self,
37 parsed_body: Body[
38 Annotated[
39 _SearchModel | XmlSearchModel,
40 conditional_type({
41 ContentType.json: SearchModel,
42 ContentType.xml: XmlSearchModel,
43 }),
44 ],
45 ],
46 ) -> str:
47 return 'post'
48
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"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usercontroller/": {
"post": {
"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(Controller[PydanticSerializer]):
20 parsers = (MultiPartParser(),)
21
22 def post(
23 self,
24 parsed_file_metadata: FileMetadata[
25 Annotated[
26 UserUpload,
27 MediaTypeMetadata(
28 # Note, that this can also inferred from `Literal` type
29 # in `FileModel.content_type` property, but can be set here:
30 encoding={'avatar': Encoding(content_type='image/png')},
31 ),
32 ]
33 ],
34 ) -> str:
35 return 'post'
36
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/usercontroller/": {
"post": {
"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"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usercontroller/": {
"post": {
"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": {
"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"
}
}
}
}
}
}
Specification extensions#
Specification extensions
are custom keys that start with x-,
OpenAPI allows them on almost every object.
All objects in dmr.openapi.objects that support them
have the x_extensions field, its keys are dumped
next to the object’s own keys.
Only Reference
and SecurityRequirement
cannot be extended, as the spec requires.
Extensions can also be set without building the objects by hand, each setting describes exactly one object:
x_extensionsfor the rootOpenAPIdocumentx_extensionsfor thePathItemof the controllerx_extensionsofmodifyandvalidatefor theOperationof the endpointParameterMetadatafor parametersMediaTypeMetadatafor request body media typesResponseSpecfor responses
Important
x_extensions is unique among our settings:
it is never inherited from a less specific level
and never merged across levels.
Other settings, like tags or servers, configure
a single operation field, so a controller value is a sensible
default for all its operations.
Extensions belong to every OpenAPI object separately:
a path item and its operations are different objects
with different extensions, so a controller value describes
the path item only, and an endpoint value describes
the operation only. Inheriting or merging them would put
path item extensions on objects they were never meant for.
1from http import HTTPStatus
2from typing import Annotated, ClassVar
3
4import msgspec
5from django.http import HttpResponse
6
7from dmr import Body, Controller, Query, ResponseSpec, modify, validate
8from dmr.openapi.objects import MediaTypeMetadata, ParameterMetadata
9from dmr.plugins.msgspec import MsgspecSerializer
10
11
12class UserModel(msgspec.Struct):
13 name: str
14
15
16class UserFilters(msgspec.Struct):
17 search: str
18
19
20class UserController(Controller[MsgspecSerializer]):
21 # Set on the path item, operations do not inherit it:
22 x_extensions: ClassVar = {'x-owner': 'users-team'}
23
24 def get(
25 self,
26 parsed_query: Query[
27 Annotated[
28 UserFilters,
29 ParameterMetadata(x_extensions={'x-searchable': True}),
30 ]
31 ],
32 ) -> list[UserModel]:
33 return [UserModel(name=parsed_query.search)]
34
35 @modify(x_extensions={'x-rate-limit': 100}) # Set on the operation
36 def post(
37 self,
38 parsed_body: Body[
39 Annotated[
40 UserModel,
41 MediaTypeMetadata(x_extensions={'x-strict': True}),
42 ]
43 ],
44 ) -> UserModel:
45 return parsed_body
46
47 @validate(
48 ResponseSpec(
49 status_code=HTTPStatus.OK,
50 return_type=UserModel,
51 x_extensions={'x-cacheable': True},
52 ),
53 )
54 def put(self) -> HttpResponse:
55 return self.to_response(UserModel(name='dmr'))
56
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": {
"name": {
"type": "string"
}
},
"required": [
"name"
],
"title": "UserModel",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usercontroller/": {
"get": {
"operationId": "getUserControllerApiUsercontroller",
"parameters": [
{
"in": "query",
"name": "search",
"required": true,
"schema": {
"type": "string"
},
"x-searchable": true
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/UserModel"
},
"type": "array"
}
}
},
"description": "OK"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
},
"post": {
"operationId": "postUserControllerApiUsercontroller",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserModel"
},
"x-strict": true
}
},
"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"
}
},
"x-rate-limit": 100
},
"put": {
"operationId": "putUserControllerApiUsercontroller",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserModel"
}
}
},
"description": "OK",
"x-cacheable": true
},
"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"
}
}
},
"x-owner": "users-team"
}
}
}
Keys must start with x-,
we let openapi-spec-validator report invalid ones.
load_schema() keeps extensions
of external schemas as well.
Added in version 0.16.0.
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(Controller[MsgspecSerializer]):
20 def post(self, parsed_query: Query[QueryModel]) -> UserModel:
21 return UserModel(uid=uuid.uuid4(), username='example')
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.",
"examples": [
{
"detail": [
{
"loc": [
"GjgxiHQOyswaLrqexEuT"
],
"msg": "bzQIBShDtFfOfiPXwvmo"
}
]
}
],
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
},
"UserModel": {
"examples": [
{
"uid": "74f942cb-220a-4b0a-9cd2-875ea96ec2b3",
"username": "NZYFcagWptUqCwdERilv"
}
],
"properties": {
"uid": {
"format": "uuid",
"type": "string"
},
"username": {
"type": "string"
}
},
"required": [
"uid",
"username"
],
"title": "UserModel",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usercontroller/": {
"post": {
"operationId": "postUserControllerApiUsercontroller",
"parameters": [
{
"in": "query",
"name": "search",
"required": true,
"schema": {
"type": "string"
}
},
{
"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.
Note
Generated examples are written to the JSON Schema examples list,
not to the OAS example keyword, which OpenAPI 3.2 deprecates
inside Schema Objects. Examples that you write by hand
are never rewritten.
Note
The seed is a global setting, it cannot be changed
per controller or per endpoint.
Generated examples are stored on shared components/schemas entries,
which several endpoints and controllers can reference at once.
See openapi_examples_seed for the reasoning.
Top level API#
This is how OpenAPI spec is generated, top level overview:
---
config:
theme: forest
---
graph
Start[build_schema] --> OpenAPIContext[OpenAPIContext];
Start[build_schema] --> Router[Router];
OpenAPIContext --> OpenAPIConfig[OpenAPIConfig];
Router -->|for each controller| Controller[Controller.get_schema];
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| SecurityRequirements[Auth.security_requirements];
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_schema()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_requirements()to 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: str = '3.1.0', json_schema_dialect: str | None = None, 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]]] | sentinel | None = EMPTY, 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, self_uri: str | None = None, x_extensions: dict[str, Any] | 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.
- version#
Version of your API (your application’s own version, not the OpenAPI spec version).
- Type:
- openapi_version#
Version of the OpenAPI specification to target. Only
'3.1.0'and newer versions are supported, because older ones are not based on JSON Schema. Defaults to'3.1.0'.- Type:
- json_schema_dialect#
Default value of the
$schemakeyword for all Schema Objects in the document, as a URI. Schemas that set$schemathemselves are not affected. Defaults toNone: the dialect of the targeted OpenAPI version is used.- Type:
str | None
- contact#
Contact information for the exposed API.
- Type:
- external_docs#
Link to additional external documentation.
- security#
Global security requirements applied across the API. It is also used as the settings level of
securityfor all controllers and endpoints, so it is merged with the requirements generated fromauth. Can be overridden per controller and per endpoint. See Customizing security.
- license#
License information for the exposed API.
- Type:
- components#
Reusable components (schemas, responses, parameters, etc.) to include in the spec.
- servers#
Connectivity information for the target servers.
- Type:
- tags#
Metadata tags used to group operations in the documentation.
- Type:
list[dmr.openapi.objects.tag.Tag] | None
- webhooks#
Webhook definitions that may be initiated by the API, keyed by name.
- x_extensions#
Specification extensions of the root document, keys must start with
x-.
- self_uri#
Self-assigned URI of the generated document, dumped as
$self. It also serves as the base URI to resolve references against. Added in OpenAPI'3.2.0'.- Type:
str | None
Changed in version 0.16.0:
openapi_versionolder than'3.1.0'now raises aValueError. Addedjson_schema_dialectattribute.Changed in version 0.16.0: Added
self_uriandx_extensions.- __post_init__() None[source]#
Validates that
openapi_versionis supported.- Raises:
ValueError – if
openapi_versionis older than'3.1.0'.
- dmr.openapi.default_config() OpenAPIConfig[source]#
Resolves the default config from settings.
- class dmr.openapi.OpenAPIContext(config: OpenAPIConfig | None = None)[source]#
Context for OpenAPI specification generation.
Maintains shared state and generators used across the OpenAPI generation process. Provides access to different generators.
Subclass this context and override the
*_clsattributes to customize schema generation. Each class is instantiated with the current context.Changed in version 0.16.0: Added class-level overrides for generators and the configuration merger.
Changed in version 0.16.0: Added the
seed_examples()hook. Now all used classes can be customized via subclassing and overriding class-level variables.- component_parsers_cls#
alias of
ComponentParserGenerator
- config_merger_cls#
alias of
ConfigMerger
- get_components() Components[source]#
Resolve all components from own and external schemas.
Added in version 0.13.0.
- operation_id_cls#
alias of
OperationIdGenerator
- operation_id_registry_cls#
alias of
OperationIdRegistry
- parameter_cls#
alias of
ParameterGenerator
- response_cls#
alias of
ResponseGenerator
- schema_cls#
alias of
SchemaGenerator
- schema_registry_cls#
alias of
SchemaRegistry
- security_scheme_cls#
alias of
SecuritySchemeGenerator
- security_scheme_registry_cls#
alias of
SecuritySchemeRegistry
- seed_examples() None[source]#
Seed the generation of examples for this schema.
One context builds one schema, so this runs exactly once per schema. All its examples then come from a single random stream, which is what makes them differ from each other.
Override it to seed from something other than
openapi_examples_seed, or to leave the factory alone entirely.Added in version 0.16.0.
- dmr.openapi.load_schema(unstructured: dict[str, Any], model: type[_DataclassT]) _DataclassT[source]#
Load unstructured schema into the model dataclass type.
Used to include external schemas into the DMR-based project. Only works with
pydanticinstalled:pip install 'django-modern-rest[pydantic]'Added in version 0.13.0.
Changed in version 0.16.0: Specification extensions, like
x-thing, are now kept inx_extensionsof the objects that support them. They used to be silently dropped.
All other objects that are only used if you decide to customize the schema are listed in OpenAPI.