Configuration#

We use DMR_SETTINGS dictionary object to store all the configuration. All keys are typed with Settings enum keys which can be used to both set and get settings.

Note

Remember, that django-modern-rest settings are cached after the first access. If you need to modify settings dynamically in runtime use clear_settings_cache(). You can modify the size of cache with adjusting DMR_MAX_CACHE_SIZE value.

Here are all keys and values that can be set. As usual, all settings go to settings.py file in your Django project.

See also

We also validate defined settings in import time. See Settings validation for more details.

Configuration levels#

Most of the settings can be also configured on the controller level with Controller attributes and on the endpoint level with modify and validate parameters.

The most specific level always wins:

  1. Endpoint values override controller values

  2. Controller values override settings values

Values are never merged, they always redefine each other.

Every value on every level defaults to EMPTY, which means “not set on this level, use the next one”. This is true for sequences, sets, booleans, and all other values. None is never a default: it is a real value that disables something, like auth=None or tags=None, and it is only allowed where disabling makes sense.

Explicit empty values are real values as well, they are taken literally: auth=[] means “no auth on this level”, it does not fall through to the next level. So, auth=[] and auth=() work exactly like auth=None does. The same is true for throttling, security, responses, tags, and all other sequences and sets. Only EMPTY uses the next level.

Here’s how one can use this system to achieve different strategies. Let’s use authentication as the example.

Overrides values from previous levels.

Run result

$ curl http://127.0.0.1:8000/api/users/ -X GET --cookie access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxNzkxMjg1NDI4LCJpYXQiOjE3OTExOTkwMjgsImp0aSI6ImYwNTJiYWFkMzQwYTQ3ZDI5NWIyMzk2MzA0N2NlODM0IiwiZXh0cmFzIjp7InR5cGUiOiJhY2Nlc3MifX0.Xnkk5cg1vvPTRT9zowxrsjhy7jPDngnV-kIBj0evl2s
"get"

$ curl http://127.0.0.1:8000/api/users/ -X GET -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxNzkxMjg1NDI5LCJpYXQiOjE3OTExOTkwMjksImp0aSI6IjBiYmIxNTVhMDAzMjQ4MmY5ODA2OGNlMWNkOTFiODg3IiwiZXh0cmFzIjp7InR5cGUiOiJhY2Nlc3MifX0.lRXdMkBXchKoX7fywkS565uAA9EOme3nVe1QgEhklXE'
{"detail":[{"msg":"Not authenticated","type":"security"}]}

$ curl http://127.0.0.1:8000/api/users/ -X POST -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxNzkxMjg1NDI5LCJpYXQiOjE3OTExOTkwMjksImp0aSI6IjA2ODE0ZDU3ZTVmZjQ0ZTc4NDkwOGUwZmJjZDNhNGE3IiwiZXh0cmFzIjp7InR5cGUiOiJhY2Nlc3MifX0.FUubMRqWZq-8Vaji3pfDnWSHY9tE5nVtg0td7jlio8s'
"post"

$ curl http://127.0.0.1:8000/api/users/ -X POST --cookie access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxNzkxMjg1NDMwLCJpYXQiOjE3OTExOTkwMzAsImp0aSI6ImNjODM1MzhmMTZhNDQ2NjJiY2Q0MzYyZWNlMWM3ZTg1IiwiZXh0cmFzIjp7InR5cGUiOiJhY2Nlc3MifX0.mFtybBnF9l8ZBWb4bmbpXOTLoWj6t8LknLugcmkGZLE
{"detail":[{"msg":"Not authenticated","type":"security"}]}

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": {
      "csrf": {
        "description": "CSRF protection",
        "in": "cookie",
        "name": "csrftoken",
        "type": "apiKey"
      },
      "jwt": {
        "bearerFormat": "JWT",
        "description": "JWT token auth",
        "scheme": "Bearer",
        "type": "http"
      },
      "jwt_cookie": {
        "description": "JWT token auth via cookie",
        "in": "cookie",
        "name": "access_token",
        "type": "apiKey"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/apicontroller/": {
      "get": {
        "operationId": "getAPIControllerApiApicontroller",
        "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": [
          {
            "jwt_cookie": []
          }
        ]
      },
      "post": {
        "operationId": "postAPIControllerApiApicontroller",
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "Created"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when auth was not successful",
            "headers": {
              "WWW-Authenticate": {
                "description": "Challenges that the client can use to authenticate this request",
                "required": true,
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "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": [
          {
            "jwt": []
          }
        ]
      }
    }
  }
}

One can customize metadata_merger_cls from the default MetadataMerger to change how values are merged into the final metadata. For example, it is possible to restore 0.15.0 behavior and merge all sequences in a custom subclass, if it is needed.

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

Settings#

Class with all possible setting keys as enum:

final class dmr.settings.Settings(*values)[source]#

Bases: StrEnum

Keys for all settings.

To get settings use resolve_setting() function together with Settings keys:

>>> from dmr.settings import Settings, resolve_setting

>>> resolve_setting(Settings.responses)
[]

To set settings use:

>>> DMR_SETTINGS = {Settings.responses: []}

Class that can be used to properly type settings in user’s code: dmr.settings.SettingsDict.

Content negotiation#

Note

It is recommended to always install msgspec with 'django-modern-rest[msgspec]' extra for better performance.

dmr.settings.Settings.parsers#

Default: dmr.parsers.JsonParser or dmr.plugins.msgspec.MsgspecJsonParser if installed.

A list of instances of subtypes of Parser to serialize data from the requested text format, like json or xml, into python object.

Custom configuration example, let’s say you want to always use orjson:

settings.py#
import orjson
from dmr.parsers import JsonParser
DMR_SETTINGS = {Settings.parsers: [JsonParser(json_module=orjson)]}

See Configuration levels on how to merge / override / disable this setting on multiple levels.

Changed in version 0.16.0: Controller and endpoint values are not merged anymore.

dmr.settings.Settings.renderers#

Default: dmr.renderers.JsonRenderer or dmr.plugins.msgspec.MsgspecJsonRenderer if installed.

A list of instances of subtypes of Renderer to serialize python objects to the requested text format, like json or xml.

Custom configuration example, let’s say you want to always use orjson:

settings.py#
import orjson
from dmr.renderers import JsonRenderer
DMR_SETTINGS = {Settings.renderers: [JsonRenderer(json_module=orjson)]}

See Configuration levels on how to merge / override / disable this setting on multiple levels.

Changed in version 0.16.0: Controller and endpoint values are not merged anymore.

dmr.settings.Settings.validate_negotiation#

Default: EMPTY

Should we validate content negotiation? Meaning: django-modern-rest finds which parser and which renderer to use based on Content-Type and Accept headers. However, by mistake people can return responses with wrong Content-Type if they construct responses manually. See Content negotiation for more info.

Defaults to the value set in validate_responses for convenience if this value is EMPTY.

To disable the content negotiation validation globally, use:

settings.py#
>>> DMR_SETTINGS = {
...     Settings.validate_negotiation: False,
... }

Response handling#

dmr.settings.Settings.responses#

Default: []

The list of global ResponseSpec object that will be added to endpoints’ metadata as a possible response schema.

Use it to set global responses’ status codes like 500:

settings.py#
>>> from http import HTTPStatus
>>> from typing_extensions import TypedDict
>>> from dmr import ResponseSpec

>>> class Error(TypedDict):
...     detail: str

>>> # If our API can always return a 500 response with `{"detail": str}`
>>> # error message:
>>> DMR_SETTINGS = {
...     Settings.responses: [
...         ResponseSpec(
...             Error,
...             status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
...         ),
...     ],
... }

See Configuration levels on how to merge / override / disable this setting on multiple levels.

Changed in version 0.16.0: Controller and endpoint values are not merged anymore.

dmr.settings.Settings.validate_responses#

Default: True

When some endpoint returns any data, we by default validate that this data matches the endpoint schema.

So, this code will produce not only static typing error, but also a runtime error:

>>> from dmr import Controller
>>> from dmr.plugins.pydantic import PydanticFastSerializer

>>> class MyController(Controller[PydanticFastSerializer]):
...     def get(self) -> list[str]:
...         return [1, 2]  # <- both static typing and runtime error

But, there’s a runtime cost to this. It is recommended to switch this validation off for production:

settings.py#
>>> DMR_SETTINGS = {Settings.validate_responses: False}

Note

You can also switch off this validation per-controller with validate_responses and per-endpoint with validate_responses argument to modify and validate.

We recommend setting this value to not DEBUG.

dmr.settings.Settings.exclude_validate_responses#

Default: frozenset()

Pass any status code that you don’t want to be validated, even when validate_responses is enabled.

Unlike describing a response, this does not add anything to the OpenAPI schema: such responses are simply returned to the client as-is. See Validating error responses for the details.

settings.py#
>>> from http import HTTPStatus

>>> DMR_SETTINGS = {
...     Settings.exclude_validate_responses: {
...         HTTPStatus.INTERNAL_SERVER_ERROR,
...     },
... }

See Configuration levels on how to merge / override / disable this setting on multiple levels.

Added in version 0.15.0.

Changed in version 0.16.0: Controller and endpoint values are not merged anymore.

Semantic schema generation#

dmr.settings.Settings.semantic_schema#

Default: True

When True, parsers, renderers, and authentication classes automatically inject their semantic schema like response specs, security schemes, and security requirements into every endpoint’s metadata. This data is then visible in the generated OpenAPI spec.

Set to False to disable this auto-injection globally. User-defined schema (via @modify, @validate, controller responses, Settings.responses, or security) are not affected by this flag. Runtime response validation still works as configured by Settings.validate_responses.

settings.py#
>>> DMR_SETTINGS = {Settings.semantic_schema: False}

Added in version 0.16.0.

dmr.settings.Settings.semantic_responses#

Default: EMPTY

When True, parsers, renderers, and authentication classes automatically inject their semantic responses spec (e.g. 422 Unprocessable Entity, 406 Not Acceptable) into every endpoint’s metadata. These responses are then visible in the generated OpenAPI spec.

Set to False to disable this auto-injection globally. User-defined responses (via @modify, @validate, controller responses, or Settings.responses) are not affected by this flag. Runtime response validation still works as configured by Settings.validate_responses.

Defaults to the value set in semantic_schema for convenience if this value is EMPTY.

settings.py#
>>> DMR_SETTINGS = {Settings.semantic_responses: False}
dmr.settings.Settings.exclude_semantic_responses#

Default: frozenset()

Pass any status code, that you wanna exclude from semantic responses.

settings.py#
>>> from http import HTTPStatus

>> DMR_SETTINGS = {
...    Settings.exclude_semantic_responses: {
...        HTTPStatus.CONFLICT,
...    },
... }

See Configuration levels on how to merge / override / disable this setting on multiple levels.

Changed in version 0.16.0: Controller and endpoint values are not merged anymore.

dmr.settings.Settings.semantic_auth#

Default: EMPTY

When True, authentication classes automatically inject their semantic security requirements and security schemes into the final OpenAPI spec.

Set to False to disable this auto-injection globally. User-defined schemes and security field (via @modify, @validate, etc) are not affected by this flag.

Defaults to the value set in semantic_schema for convenience if this value is EMPTY.

settings.py#
>>> DMR_SETTINGS = {Settings.semantic_auth: False}

Added in version 0.16.0.

dmr.settings.Settings.exclude_semantic_auth#

Default: frozenset()

Exclude specific semantic security schemes and security requirements that auth instances produce from the spec.

settings.py#
>>> DMR_SETTINGS = {Settings.exclude_semantic_auth: {'jwt'}}

See Configuration levels on how to merge / override / disable this setting on multiple levels.

Added in version 0.16.0.

dmr.settings.Settings.semantic_schema_providers#

Default: [ResponseValidationSpecProvider(), CSRFSemanticSchemaProvider()]

Sequence of default system-wide semantic schema providers. By default we use ResponseValidationSpecProvider and CSRFSemanticSchemaProvider to provide common responses and auth specs that can happen with all endpoints. Each endpoint will have these parts of the semantic schema, if their conditions are met.

For example, CSRFSemanticSchemaProvider will inject for controller that have csrf_exempt=False: - 403 status code response spec - csrf security requirement to all auth instances - Global csrf security scheme component

exclude_semantic_responses and exclude_semantic_auth are also respected.

settings.py#
>>> from http import HTTPStatus
>>> from dmr.security.csrf import CSRFSemanticSchemaProvider

>> DMR_SETTINGS = {
...    Settings.semantic_schema_providers: [
...        CSRFSemanticSchemaProvider(description='CSRF error'),
...    ],
... }

You can customize instances of the response spec providers or add new ones.

Added in version 0.16.0.

Error handling#

dmr.settings.Settings.global_error_handler#

Default: 'dmr.errors.global_error_handler'

Globally handle all errors in the application. You can use real object or string path for the object to be imported. Here’s our error handling hierarchy:

  1. Per-endpoint with handle_error() and handle_async_error()

  2. Per-controller with handle_error() or handle_async_error()

  3. If nothing helped, 'global_error_handler' is called

See global_error_handler() for the callback type.

settings.py#
>>> DMR_SETTINGS = {Settings.global_error_handler: 'path.to.your.handler'}

Authentication#

dmr.settings.Settings.auth#

Default: []

Configure authentication rules for the whole API.

To enable auth for all endpoints you can use:

settings.py#
>>> from dmr.security.django_session import DjangoSessionSyncAuth

>>> DMR_SETTINGS = {
...     Settings.auth: [
...         DjangoSessionSyncAuth(),
...     ],
... }

If you use both sync and async controllers in your app, consider using SyncOrAsyncAuth for settings. All auth types must be importable in settings.

See Configuration levels on how to merge / override / disable this setting on multiple levels.

Changed in version 0.16.0: Controller and endpoint values are not merged anymore.

Throttling#

dmr.settings.Settings.throttling#

Default: []

Configure throttling rules for the whole API.

To enable throttling for all endpoints you can use:

settings.py#
>>> from dmr.settings import Settings
>>> from dmr.throttling import SyncThrottle, Rate

>>> DMR_SETTINGS = {
...     Settings.throttling: [SyncThrottle(10, Rate.second)],
... }

If you use both sync and async controllers in your app, consider using SyncOrAsyncThrottle for settings. All throttle types must be importable in settings.

See Configuration levels on how to merge / override / disable this setting on multiple levels.

Changed in version 0.16.0: Controller and endpoint values are not merged anymore.

HTTP Spec validation#

dmr.settings.Settings.no_validate_http_spec#

Default: frozenset()

A set of unique HttpSpec codes to be globally disabled.

We don’t recommend disabling any of these checks globally.

settings.py#
>>> from dmr.settings import HttpSpec

>>> DMR_SETTINGS = {
...     Settings.no_validate_http_spec: {
...         HttpSpec.empty_request_body,
...     },
... }

See Configuration levels on how to merge / override / disable this setting on multiple levels.

Changed in version 0.16.0: Controller and endpoint values are not merged anymore.

final class dmr.settings.HttpSpec(*values)[source]#

Bases: StrEnum

Keys for our HTTP spec validation.

All rules can be disabled per endpoint and per controller. You can disable any of the validation rules we have here globally by:

>>> DMR_SETTINGS = {
...     Settings.no_validate_http_spec: {
...         HttpSpec.empty_response_body,
...     },
... }
empty_request_body#

Disables validation that methods like GET and HEAD can’t have request bodies.

empty_response_body#

Disables validation that some status codes like 204 must not have response bodies.

header_name_server_managed#

Disables validation that headers like Server, Keep-Alive, etc. can’t be included in response headers.

header_name_syntax#

Disables validations that check name syntax to avoid headers like X Custom Name or @@@.

cookie_name_syntax#

Disables validation that check name syntax for cookie names to avoid disallowed characters.

cookie_semantics#

Disables validation that check if cookies semantix container proper values, like max_age must not be negative, or samesite=’none’ requires secure=True, etc

Streaming#

dmr.settings.Settings.validate_events#

Default: EMPTY

Should we validate the events in all streams? Defaults to the value set in validate_responses for convenience if this value is EMPTY.

To disable the event validation globally, use:

settings.py#
>>> DMR_SETTINGS = {
...     Settings.validate_events: False,
... }

OpenAPI#

dmr.settings.Settings.openapi_config#

Default: OpenAPIConfig(title='Your Awesome Project', version='0.1.0')

Metadata to be used in the OpenAPI schema.

See OpenAPIConfig for the available fields and their description. It can also be used to change the default OpenAPI spec version. Its security field is also the settings level of security for all controllers and endpoints, see Customizing security.

settings.py#
>>> from dmr.openapi.config import OpenAPIConfig

>>> DMR_SETTINGS = {
...     Settings.openapi_config: OpenAPIConfig(
...         title='My Amazing App',
...         version='13.22.3',
...         openapi_version='3.2.0',
...     ),
... }
dmr.settings.Settings.openapi_examples_seed#

Default: EMPTY

Random seed to use when generating missing examples in the OpenAPI spec.

If set to EMPTY, no examples are generated. Existing examples won’t be overridden.

It only works if 'django-modern-rest[openapi]' extra is installed. If polyfactory package is missing, no examples are generated.

settings.py#
>>> DMR_SETTINGS = {
...     Settings.openapi_examples_seed: 10,
... }

Note

This 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, so there’s no single endpoint to read a local seed from. See Examples generation.

dmr.settings.Settings.openapi_static_cdn#

Default: {}

Optional mapping to switch OpenAPI renderers to CDN resources. Only renderers explicitly listed in this mapping will use CDN, the rest will use local bundled static files served by Django.

Supported keys are:

  • swagger: base URL to swagger-ui-dist (without file name)

  • redoc: full URL to redoc.standalone.js

  • scalar: full URL to @scalar/api-reference standalone bundle

  • stoplight: base URL to @stoplight/elements (without file name)

You can also modify the exact versions that we use for each tool this way.

settings.py#
>>> DMR_SETTINGS = {
...     Settings.openapi_static_cdn: {
...         'swagger': 'https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.32.1',
...         'redoc': 'https://cdn.redoc.ly/redoc/2.5.2/bundles/redoc.standalone.js',
...         'scalar': 'https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.49.2/dist/browser/standalone.js',
...         'stoplight': 'https://unpkg.com/@stoplight/elements@9.0.16',
...     },
... }

Hacks#

dmr.settings.Settings.django_treat_as_post#

Default: frozenset({'PUT', 'PATCH'})

By default Django only populates django.http.HttpRequest.POST and django.http.HttpRequest.FILES for POST requests.

Some parsers like MultiPartParser require .POST and .FILES to be set to work with Body and FileMetadata.

However, we build REST APIs where more methods are in use, not just POST. So, we use this setting to populate .POST and .FILES when parser is a subtype of either SupportsFileParsing or SupportsDjangoDefaultParsing and we work with components that require request body.

Note

This is a really advanced setting. If you are not creating your own components or your own parsers - do not change it.

Environment variables#

DMR_MAX_CACHE_SIZE#

Default: 1024

We use functools.lru_cache() in many places internally. For example:

You can control the size / memory usage with this setting.

Increase if you have a lot of different return types or if your clients send a lot of distinct Accept headers.

DMR_USE_COMPILED#

Default: 1

We compile some modules to C-extensions with mypyc compilation. If you want to disable the extensions and fallback to pure Python implementation, set this variable to 0.

It is only recommended for debugging. It should be set to 1 in production for maximum speed.

API Reference#

dmr.settings.resolve_setting(setting_name: Settings, *, import_string: bool = False) → Any[source]#

Resolves setting by setting_name.

The result is cached using @lru_cache for performance. When testing with custom settings, you must call clear_settings_cache() before and after modifying Django settings to ensure the cache is invalidated properly.

dmr.settings.clear_settings_cache() → None[source]#

Clears settings cache for all functions in this module.

Useful for tests, when you modify the global settings object.

class dmr.settings.SettingsDict[source]#

Settings type that can be used for typing.