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:

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:

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.

Swagger view

What happens in the example above?

  1. We create / take an existing API dmr.routing.Router instance and create an OpenAPI schema from it using build_schema()

  2. Next, we define regular Django views that will serve you the API renderers

  3. You can modify these views to require auth / role / permissions / etc as 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:

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' to INSTALLED_APPS, so Django can discover the bundled renderer templates

  • If you serve bundled assets locally, add 'django.contrib.staticfiles' to INSTALLED_APPS

  • Configure 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:

settings.py#
>>> 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:

  • SwaggerView is usually the best default when you want interactive docs with “try it out” support.

  • RedocView is a good fit for mostly read-only, reference-style documentation.

  • ScalarView and StoplightView are 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) or yaml

  • --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:

  1. By defining dmr.settings.Settings.openapi_config setting inside DMR_SETTINGS in your settings.py

  2. By passing OpenAPIConfig instance into build_schema()

For example, this is how you can change some OpenAPI metadata, including the spec version:

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:

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://msgspec.dev/jsonschema

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.

Docs: https://msgspec.dev/jsonschema

Customizing path items#

Controller allows customizing some metadata for PathItem:

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.

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.

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:

  1. In settings with security of OpenAPIConfig, it applies to all endpoints of your app and it is also used as the document-level security

  2. On a controller with the security attribute, it applies to all endpoints of this controller

  3. On an endpoint with security of @modify and @validate

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=None or security=[] on an endpoint or a controller disables all less specific levels, exactly like auth=None does. Without auth such an operation gets security: [], which opts out of the document-level requirements instead of inheriting them

  • The resolved requirements are merged with the requirements generated from auth by security_merger. By default, they are added as-is after the auth ones, each requirement is an alternative: a client can satisfy any of them

  • The default merger raises EndpointMetadataError during the schema generation when the same requirement is present twice, for example, when security repeats what auth already generates

  • Security schemes used in security are not registered automatically, declare them in components of OpenAPIConfig. Schemes that auth registers can be reused in security

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:

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 documentation

  • deprecated: 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 tags#

Tags can be defined on three levels:

  1. On a router with the tags parameter

  2. On a controller with the tags attribute, it applies to all endpoints of this controller

  3. On an endpoint with the tags parameter of modify or validate

The most specific level wins, tags are not merged: endpoint tags override controller tags, controller tags override router tags. Set tags=None to have no tags at all. To merge tags from different levels, do it explicitly:

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/": {
      "get": {
        "operationId": "getUserControllerApiUsercontroller",
        "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": [
          "users"
        ]
      },
      "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"
          }
        },
        "tags": [
          "admin"
        ]
      },
      "put": {
        "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": [
          "users",
          "admin"
        ]
      }
    }
  }
}

Added in version 0.16.0: Controller-level tags.

Changed in version 0.16.0: Tags from different levels used to be merged.

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:

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:

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:

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:

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:

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:

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.

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.

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:

  1. Regular user-facing objects must know how to build the OpenAPI schema. For example: Endpoint, Controller, and Router all know how to build the spec for themselves. Since they are user-facing, it is easy to modify how the schema generation works if needed

  2. All model schemas must be directly generated by their libraries. We don’t do anything with the JSON Schema that is generated by pydantic or msgspec. They can do a better job than we do. However, their schemas still can be customized. See PydanticSchemaGenerator and MsgspecSchemaGenerator

APIs for schema overrides#

Useful APIs for users to override:

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.

title#

Human-readable title of the API, shown in the generated documentation.

Type:

str

version#

Version of your API (your application’s own version, not the OpenAPI spec version).

Type:

str

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:

str

json_schema_dialect#

Default value of the $schema keyword for all Schema Objects in the document, as a URI. Schemas that set $schema themselves are not affected. Defaults to None: the dialect of the targeted OpenAPI version is used.

Type:

str | None

summary#

Short, one-line summary of the API.

Type:

str | None

description#

Longer description of the API. May use CommonMark syntax.

Type:

str | None

terms_of_service#

URL to the terms of service for the API.

Type:

str | None

contact#

Contact information for the exposed API.

Type:

dmr.openapi.objects.contact.Contact | None

external_docs#

Link to additional external documentation.

Type:

dmr.openapi.objects.external_documentation.ExternalDocumentation | None

security#

Global security requirements applied across the API. It is also used as the settings level of security for all controllers and endpoints, so it is merged with the requirements generated from auth. Can be overridden per controller and per endpoint. See Customizing security.

Type:

list[dict[str, list[str]]] | typing_extensions.sentinel | None

license#

License information for the exposed API.

Type:

dmr.openapi.objects.license.License | None

components#

Reusable components (schemas, responses, parameters, etc.) to include in the spec.

Type:

dmr.openapi.objects.components.Components | list[dmr.openapi.objects.components.Components] | None

servers#

Connectivity information for the target servers.

Type:

list[dmr.openapi.objects.server.Server] | None

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.

Type:

dict[str, dmr.openapi.objects.path_item.PathItem | dmr.openapi.objects.reference.Reference] | None

x_extensions#

Specification extensions of the root document, keys must start with x-.

Type:

dict[str, Any] | None

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_version older than '3.1.0' now raises a ValueError. Added json_schema_dialect attribute.

Changed in version 0.16.0: Added self_uri and x_extensions.

__post_init__() → None[source]#

Validates that openapi_version is supported.

Raises:

ValueError – if openapi_version is older than '3.1.0'.

property openapi_version_info: tuple[int, int, int]#

Returns the parsed OpenAPI version.

Added in version 0.8.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 *_cls attributes 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 pydantic installed:

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 in x_extensions of 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.