OpenAPI

We support OpenAPI versions from 3.0.0 through 3.2.0.

Note

By default, we use OpenAPI 3.1.0, since tooling such as Swagger, Scalar, Redoc, and Stoplight does not yet fully support the latest specification. You can track the current progress here.

Setting up OpenAPI views

We support:

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"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/user/": {
      "post": {
        "deprecated": false,
        "operationId": "postUsercontrollerApiUser",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserCreateModel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserModel"
                }
              }
            },
            "description": "Created"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when request components cannot be parsed"
          },
          "406": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when provided `Accept` header cannot be satisfied"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when returned response does not match the response schema"
          }
        }
      }
    }
  }
}

And then visit https://localhost:8000/docs/swagger/ (or any other renderer) for the interactive docs.

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

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:

  • --formatjson (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"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "My awesome API",
    "version": "1.0.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/user/": {
      "post": {
        "deprecated": false,
        "operationId": "postUsercontrollerApiUser",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserCreateModel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserModel"
                }
              }
            },
            "description": "Created"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when request components cannot be parsed"
          },
          "406": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when provided `Accept` header cannot be satisfied"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when returned response does not match the response schema"
          }
        }
      }
    }
  },
  "servers": [
    {
      "url": "https://prod.example.com"
    },
    {
      "url": "https://dev.example.com"
    }
  ]
}

Customizing OpenAPI generation

Customizing schema

We delegate all schema generation to the model’s library directly. To do so, we use BaseSchemaGenerator subclasses for different serializers.

To customize a schema, use the native methods.

Docs: https://jcristharif.com/msgspec/jsonschema.html

Note

By default docstring or __doc__ from the model is used as a description.

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"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "description": "Create new users",
      "post": {
        "deprecated": false,
        "operationId": "postUsercontrollerApiUsercontroller",
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "Created"
          },
          "406": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when provided `Accept` header cannot be satisfied"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when returned response does not match the response schema"
          }
        }
      },
      "servers": [
        {
          "url": "https://example.com"
        },
        {
          "url": "https://dev.example.com"
        }
      ]
    }
  }
}

Note

By default docstring or __doc__ from the controller is used to generate summary and description for the PathItem.

Customizing operation

@modify and @validate can be used to customize the resulting Operation metadata.

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {
      "ErrorDetail": {
        "description": "Base schema for error details description.",
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string"
                }
              ]
            },
            "type": "array"
          },
          "msg": {
            "type": "string"
          },
          "type": {
            "type": "string"
          }
        },
        "required": [
          "msg"
        ],
        "title": "ErrorDetail",
        "type": "object"
      },
      "ErrorModel": {
        "description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ErrorDetail"
            },
            "type": "array"
          }
        },
        "required": [
          "detail"
        ],
        "title": "ErrorModel",
        "type": "object"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "post": {
        "deprecated": false,
        "operationId": "postUsercontrollerApiUsercontroller",
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "Created"
          },
          "406": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when provided `Accept` header cannot be satisfied"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when returned response does not match the response schema"
          }
        },
        "servers": [
          {
            "url": "https://example.com"
          }
        ]
      },
      "put": {
        "deprecated": false,
        "description": "PUT operation description",
        "operationId": "putUsercontrollerApiUsercontroller",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "OK"
          },
          "406": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when provided `Accept` header cannot be satisfied"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when returned response does not match the response schema"
          }
        },
        "tags": [
          "Public"
        ]
      }
    }
  }
}

Note

By default docstring or __doc__ from endpoint’s function definition is used to generate summary and description for the Operation.

Customizing router-level metadata

Router supports tags and deprecated parameters to apply OpenAPI metadata to all operations in the router:

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {
      "ErrorDetail": {
        "description": "Base schema for error details description.",
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string"
                }
              ]
            },
            "type": "array"
          },
          "msg": {
            "type": "string"
          },
          "type": {
            "type": "string"
          }
        },
        "required": [
          "msg"
        ],
        "title": "ErrorDetail",
        "type": "object"
      },
      "ErrorModel": {
        "description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ErrorDetail"
            },
            "type": "array"
          }
        },
        "required": [
          "detail"
        ],
        "title": "ErrorModel",
        "type": "object"
      },
      "UserCreateModel": {
        "properties": {
          "email": {
            "type": "string"
          }
        },
        "required": [
          "email"
        ],
        "title": "UserCreateModel",
        "type": "object"
      },
      "UserModel": {
        "properties": {
          "email": {
            "type": "string"
          },
          "uid": {
            "format": "uuid",
            "type": "string"
          }
        },
        "required": [
          "email",
          "uid"
        ],
        "title": "UserModel",
        "type": "object"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/v1/users/": {
      "post": {
        "deprecated": true,
        "operationId": "postUsercontrollerApiV1Users",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserCreateModel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserModel"
                }
              }
            },
            "description": "Created"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when request components cannot be parsed"
          },
          "406": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when provided `Accept` header cannot be satisfied"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when returned response does not match the response schema"
          }
        },
        "tags": [
          "users"
        ]
      }
    },
    "/api/v1/users/{user_id}/": {
      "post": {
        "deprecated": true,
        "operationId": "postUsercontrollerApiV1UsersUserId",
        "parameters": [
          {
            "deprecated": false,
            "in": "path",
            "name": "user_id",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserCreateModel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserModel"
                }
              }
            },
            "description": "Created"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when request components cannot be parsed"
          },
          "406": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when provided `Accept` header cannot be satisfied"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when returned response does not match the response schema"
          }
        },
        "tags": [
          "users"
        ]
      }
    }
  }
}
  • tags: List of strings to group operations in OpenAPI documentation

  • deprecated: Boolean flag to mark all operations in this router as deprecated

These router-level settings are automatically merged with endpoint-level customizations set via @modify or @validate. Router tags are prepended to endpoint tags, and deprecated is set to True if either the router or endpoint has it enabled.

You can also set tags and deprecated at the individual endpoint level via @modify to override or extend router-level settings.

Customizing parameter

There are different styles and other features that Parameter supports in OpenAPI Parameters.

For example, if you want to change how Query parameter is documented with the help of dmr.openapi.objects.ParameterMetadata annotation:

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {
      "ErrorDetail": {
        "description": "Base schema for error details description.",
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string"
                }
              ]
            },
            "type": "array"
          },
          "msg": {
            "type": "string"
          },
          "type": {
            "type": "string"
          }
        },
        "required": [
          "msg"
        ],
        "title": "ErrorDetail",
        "type": "object"
      },
      "ErrorModel": {
        "description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ErrorDetail"
            },
            "type": "array"
          }
        },
        "required": [
          "detail"
        ],
        "title": "ErrorModel",
        "type": "object"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "post": {
        "deprecated": false,
        "operationId": "postUsercontrollerApiUsercontroller",
        "parameters": [
          {
            "deprecated": true,
            "description": "Old way to search things",
            "in": "query",
            "name": "search",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "deprecated": true,
            "description": "Old way to search things",
            "in": "query",
            "name": "max_items",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "Created"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when request components cannot be parsed"
          },
          "406": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when provided `Accept` header cannot be satisfied"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when returned response does not match the response schema"
          }
        }
      }
    }
  }
}

Customizing media types

There are different metadata fields, like examples and encoding, that MediaType supports in OpenAPI MediaType.

For example, if you want to change how Body provides examples, you can use dmr.openapi.objects.MediaTypeMetadata annotation:

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {
      "ErrorDetail": {
        "description": "Base schema for error details description.",
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string"
                }
              ]
            },
            "title": "Loc",
            "type": "array"
          },
          "msg": {
            "title": "Msg",
            "type": "string"
          },
          "type": {
            "title": "Type",
            "type": "string"
          }
        },
        "required": [
          "msg"
        ],
        "title": "ErrorDetail",
        "type": "object"
      },
      "ErrorModel": {
        "description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ErrorDetail"
            },
            "title": "Detail",
            "type": "array"
          }
        },
        "required": [
          "detail"
        ],
        "title": "ErrorModel",
        "type": "object"
      },
      "SearchModel": {
        "properties": {
          "max_items": {
            "title": "Max Items",
            "type": "integer"
          },
          "search": {
            "title": "Search",
            "type": "string"
          }
        },
        "required": [
          "search",
          "max_items"
        ],
        "title": "SearchModel",
        "type": "object"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "post": {
        "deprecated": false,
        "operationId": "postUsercontrollerApiUsercontroller",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "max_items": 10,
                "search": "example"
              },
              "schema": {
                "$ref": "#/components/schemas/SearchModel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "Created"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when request components cannot be parsed"
          },
          "406": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when provided `Accept` header cannot be satisfied"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when returned response does not match the response schema"
          }
        }
      }
    }
  }
}

We also support the same way for conditional types:

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {
      "ErrorDetail": {
        "description": "Base schema for error details description.",
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string"
                }
              ]
            },
            "title": "Loc",
            "type": "array"
          },
          "msg": {
            "title": "Msg",
            "type": "string"
          },
          "type": {
            "title": "Type",
            "type": "string"
          }
        },
        "required": [
          "msg"
        ],
        "title": "ErrorDetail",
        "type": "object"
      },
      "ErrorModel": {
        "description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ErrorDetail"
            },
            "title": "Detail",
            "type": "array"
          }
        },
        "required": [
          "detail"
        ],
        "title": "ErrorModel",
        "type": "object"
      },
      "XmlSearchModel": {
        "properties": {
          "search": {
            "title": "Search",
            "type": "string"
          }
        },
        "required": [
          "search"
        ],
        "title": "XmlSearchModel",
        "type": "object"
      },
      "_SearchModel": {
        "properties": {
          "max_items": {
            "title": "Max Items",
            "type": "integer"
          },
          "search": {
            "title": "Search",
            "type": "string"
          }
        },
        "required": [
          "search",
          "max_items"
        ],
        "title": "_SearchModel",
        "type": "object"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "post": {
        "deprecated": false,
        "operationId": "postUsercontrollerApiUsercontroller",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "max_items": 10,
                "search": "example"
              },
              "schema": {
                "$ref": "#/components/schemas/_SearchModel"
              }
            },
            "application/xml": {
              "schema": {
                "$ref": "#/components/schemas/XmlSearchModel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "Created"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when request components cannot be parsed"
          },
          "406": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when provided `Accept` header cannot be satisfied"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when returned response does not match the response schema"
          }
        }
      }
    }
  }
}

And for FileMetadata:

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {
      "ErrorDetail": {
        "description": "Base schema for error details description.",
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string"
                }
              ]
            },
            "title": "Loc",
            "type": "array"
          },
          "msg": {
            "title": "Msg",
            "type": "string"
          },
          "type": {
            "title": "Type",
            "type": "string"
          }
        },
        "required": [
          "msg"
        ],
        "title": "ErrorDetail",
        "type": "object"
      },
      "ErrorModel": {
        "description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ErrorDetail"
            },
            "title": "Detail",
            "type": "array"
          }
        },
        "required": [
          "detail"
        ],
        "title": "ErrorModel",
        "type": "object"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "post": {
        "deprecated": false,
        "operationId": "postUsercontrollerApiUsercontroller",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "encoding": {
                "avatar": {
                  "contentType": "image/png"
                }
              },
              "schema": {
                "properties": {
                  "avatar": {
                    "format": "binary",
                    "type": "string"
                  }
                },
                "required": [
                  "avatar"
                ],
                "title": "UserUpload",
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "Created"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when request components cannot be parsed"
          },
          "406": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when provided `Accept` header cannot be satisfied"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when returned response does not match the response schema"
          }
        }
      }
    }
  }
}

Customizing response

ResponseSpec supports all the metadata fields that Response has.

Providing an explicit Link for schemathesis stateful API testing would look like so:

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {
      "ErrorDetail": {
        "description": "Base schema for error details description.",
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string"
                }
              ]
            },
            "type": "array"
          },
          "msg": {
            "type": "string"
          },
          "type": {
            "type": "string"
          }
        },
        "required": [
          "msg"
        ],
        "title": "ErrorDetail",
        "type": "object"
      },
      "ErrorModel": {
        "description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ErrorDetail"
            },
            "type": "array"
          }
        },
        "required": [
          "detail"
        ],
        "title": "ErrorModel",
        "type": "object"
      },
      "UserModel": {
        "properties": {
          "uid": {
            "format": "uuid",
            "type": "string"
          }
        },
        "required": [
          "uid"
        ],
        "title": "UserModel",
        "type": "object"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "post": {
        "deprecated": false,
        "operationId": "postUsercontrollerApiUsercontroller",
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserModel"
                }
              }
            },
            "description": "This is a description for your response",
            "links": {
              "GetUser": {
                "operationId": "getUser",
                "parameters": {
                  "userId": "$response.body#/uid"
                }
              }
            }
          },
          "406": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when provided `Accept` header cannot be satisfied"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when returned response does not match the response schema"
          }
        }
      },
      "put": {
        "deprecated": false,
        "operationId": "putUsercontrollerApiUsercontroller",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserModel"
                }
              }
            },
            "description": "OK",
            "links": {
              "GetUser": {
                "operationId": "getUser",
                "parameters": {
                  "userId": "$response.body#/uid"
                }
              }
            }
          },
          "406": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when provided `Accept` header cannot be satisfied"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when returned response does not match the response schema"
          }
        }
      }
    }
  }
}

Examples generation

If you installed 'django-modern-rest[openapi]' extra and enabled openapi_examples_seed setting, we will generate missing examples in your OpenAPI schemas using polyfactory.

They will not have the best data quality, since they are clearly autogenerated from fake data, but sometimes it is better than nothing.

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {
      "ErrorDetail": {
        "description": "Base schema for error details description.",
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string"
                }
              ]
            },
            "type": "array"
          },
          "msg": {
            "type": "string"
          },
          "type": {
            "type": "string"
          }
        },
        "required": [
          "msg"
        ],
        "title": "ErrorDetail",
        "type": "object"
      },
      "ErrorModel": {
        "description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
        "example": {
          "detail": [
            {
              "loc": [
                "vbzQIBShDtFfOfiPXwvm"
              ],
              "msg": "gNZYFcagWptUqCwdERil"
            }
          ]
        },
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ErrorDetail"
            },
            "type": "array"
          }
        },
        "required": [
          "detail"
        ],
        "title": "ErrorModel",
        "type": "object"
      },
      "UserModel": {
        "example": {
          "uid": "7b89296c-6dcb-4c50-8857-7eb1924770d3",
          "username": "EkQQHiBrmXZcSFtoJxJI"
        },
        "properties": {
          "uid": {
            "format": "uuid",
            "type": "string"
          },
          "username": {
            "type": "string"
          }
        },
        "required": [
          "uid",
          "username"
        ],
        "title": "UserModel",
        "type": "object"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "post": {
        "deprecated": false,
        "operationId": "postUsercontrollerApiUsercontroller",
        "parameters": [
          {
            "deprecated": false,
            "in": "query",
            "name": "search",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "deprecated": false,
            "in": "query",
            "name": "max_items",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserModel"
                }
              }
            },
            "description": "Created"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when request components cannot be parsed"
          },
          "406": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when provided `Accept` header cannot be satisfied"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when returned response does not match the response schema"
          }
        }
      }
    }
  }
}

Important

However, we recommend adding semantic named examples by hand.

Top level API

This is how OpenAPI spec is generated, top level overview:

        ---
config:
  theme: forest

---
graph
    Start[build_schema] --> Router[Router];
    Router -->|for each controller| Controller[Controller.get_path_item];
    Router -->|for each defined auth| SecurityScheme[Auth.security_scheme];
    Controller -->|for each endpoint| Endpoint[Endpoint.get_schema];
    Endpoint -->|for each component| ComponentParser[ComponentParser.get_schema]
    Endpoint -->|for each response| ResponseSpec[ResponseSpec.get_schema];
    Endpoint -->|for each used auth| SecurityRequirement[Auth.security_requirement];
    ComponentParser -->|for each schema| Schema[serializer.schema_generator.get_schema];
    ResponseSpec -->|for each schema| Schema[serializer.schema_generator.get_schema];
    

OpenAPI spec generation

We have several major design principles that define our API:

  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: Literal['3.0.0', '3.1.0', '3.2.0'] = '3.1.0', summary: str | None = None, description: str | None = None, terms_of_service: str | None = None, contact: Contact | None = None, external_docs: ExternalDocumentation | None = None, security: list[dict[str, list[str]]] | None = None, license: License | None = None, components: Components | list[Components] | None = None, servers: list[Server] | None = None, tags: list[Tag] | None = None, webhooks: dict[str, PathItem | Reference] | None = None)[source]

Configuration class for customizing OpenAPI specification metadata.

This class provides a way to configure various aspects of the OpenAPI specification that will be generated for your API documentation. It allows you to customize the API information, contact details, licensing, security requirements, and other metadata that appears in the generated OpenAPI spec.

property openapi_version_info: tuple[int, int, int]

Returns the parsed OpenAPI version.

Added in version 0.8.0.

class dmr.openapi.OpenAPIContext(config: OpenAPIConfig)[source]

Context for OpenAPI specification generation.

Maintains shared state and generators used across the OpenAPI generation process. Provides access to different generators.

register_schema(annotation: Any, schema: Schema | Reference | SchemaCallback, *, override: bool = False) None[source]

Register top-level annotation resolution into an OpenAPI schema.

You can pass either a schema object itself, a reference, or a callback that returns schema, reference, or None to fallback to the default schema resolution process.

Warning

This only works for the top-level annotations with direct matches. For example: when you register User to have a specific schema, it will take effect only in cases where User is used directly. list[User] will use the default serializer schema resolution strategy.

All other objects that are only used if you decide to customize the schema are listed in OpenAPI Reference.