Response validation¶
By default, all responses (not just requests!) are validated at runtime to match the schema. This allows us to be super strict about schema generation as a pro, but as a con, it is slower than it could possibly be.
So, you can disable response validation via configuration:
Warning
Disabling response validation makes sense only in production for better performance.
It is not recommended to disable response validation for any other reason. Instead, fix your schema errors!
Keep it on in development, but disable it in production to get the best of both worlds.
1from http import HTTPStatus
2
3import msgspec
4
5from dmr import APIError, Body, Controller, Headers
6from dmr.errors import ErrorType
7from dmr.plugins.msgspec import MsgspecSerializer
8
9
10class UserModel(msgspec.Struct):
11 email: str
12
13
14class HeaderModel(msgspec.Struct):
15 consumer: str = msgspec.field(name='X-API-Consumer')
16
17
18class UserController(Controller[MsgspecSerializer]):
19 def post(
20 self,
21 parsed_body: Body[UserModel],
22 parsed_headers: Headers[HeaderModel],
23 ) -> UserModel:
24 if parsed_headers.consumer != 'my-api':
25 # Notice that this response is never documented in the spec,
26 # so, it will raise an error when validation is enabled (default).
27 raise APIError(
28 self.format_error(
29 'Wrong API consumer',
30 error_type=ErrorType.user_msg,
31 ),
32 status_code=HTTPStatus.PAYMENT_REQUIRED,
33 )
34 # This response will be documented by default:
35 return parsed_body
36
Run result
$ curl http://127.0.0.1:8000/api/user/ -X POST -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json' -H 'X-API-Consumer: my-api'
{"email":"user@wms.org"}
$ curl http://127.0.0.1:8000/api/user/ -D - -X POST -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json' -H 'X-API-Consumer: not-my-api'
HTTP/1.1 422 Unprocessable Entity
date: Sun, 26 Apr 2026 21:12:31 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 143
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Returned status code 402 is not specified in the list of allowed status codes: [201, 400, 422, 406]","type":"value_error"}]}
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": {
"email": {
"type": "string"
}
},
"required": [
"email"
],
"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": "header",
"name": "X-API-Consumer",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserModel"
}
}
},
"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"
}
}
}
}
}
}
1from http import HTTPStatus
2
3import msgspec
4
5from dmr import APIError, Body, Controller, Headers, modify
6from dmr.errors import ErrorType
7from dmr.plugins.msgspec import MsgspecSerializer
8
9
10class UserModel(msgspec.Struct):
11 email: str
12
13
14class HeaderModel(msgspec.Struct):
15 consumer: str = msgspec.field(name='X-API-Consumer')
16
17
18class UserController(Controller[MsgspecSerializer]):
19 @modify(validate_responses=False) # <- now, we won't validate this endpoint
20 def post(
21 self,
22 parsed_body: Body[UserModel],
23 parsed_headers: Headers[HeaderModel],
24 ) -> UserModel:
25 if parsed_headers.consumer != 'my-api':
26 # Notice that this response is never documented in the spec,
27 # but, it won't raise a validation error, because validation is off
28 raise APIError(
29 self.format_error(
30 'Wrong API consumer',
31 error_type=ErrorType.user_msg,
32 ),
33 status_code=HTTPStatus.PAYMENT_REQUIRED,
34 )
35 # This response will be documented by default:
36 return parsed_body
37
Run result
$ curl http://127.0.0.1:8000/api/user/ -X POST -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json' -H 'X-API-Consumer: my-api'
{"email":"user@wms.org"}
$ curl http://127.0.0.1:8000/api/user/ -D - -X POST -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json' -H 'X-API-Consumer: not-my-api'
HTTP/1.1 402 Payment Required
date: Sun, 26 Apr 2026 21:12:32 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 59
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Wrong API consumer","type":"user_msg"}]}
1from http import HTTPStatus
2
3import msgspec
4
5from dmr import APIError, Body, Controller, Headers
6from dmr.errors import ErrorType
7from dmr.plugins.msgspec import MsgspecSerializer
8
9
10class UserModel(msgspec.Struct):
11 email: str
12
13
14class HeaderModel(msgspec.Struct):
15 consumer: str = msgspec.field(name='X-API-Consumer')
16
17
18class UserController(Controller[MsgspecSerializer]):
19 # Now, we won't validate all endpoints in this controller:
20 validate_responses = False
21
22 def post(
23 self,
24 parsed_body: Body[UserModel],
25 parsed_headers: Headers[HeaderModel],
26 ) -> UserModel:
27 if parsed_headers.consumer != 'my-api':
28 # Notice that this response is never documented in the spec,
29 # but, it won't raise a validation error, because validation is off
30 raise APIError(
31 self.format_error(
32 'Wrong API consumer',
33 error_type=ErrorType.user_msg,
34 ),
35 status_code=HTTPStatus.PAYMENT_REQUIRED,
36 )
37 # This response will be documented by default:
38 return parsed_body
39
Run result
$ curl http://127.0.0.1:8000/api/user/ -X POST -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json' -H 'X-API-Consumer: my-api'
{"email":"user@wms.org"}
$ curl http://127.0.0.1:8000/api/user/ -D - -X POST -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json' -H 'X-API-Consumer: not-my-api'
HTTP/1.1 402 Payment Required
date: Sun, 26 Apr 2026 21:12:32 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 59
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Wrong API consumer","type":"user_msg"}]}
See dmr.settings.Settings.validate_responses.
>>> from dmr.settings import Settings
>>> DMR_SETTINGS = {Settings.validate_responses: False}
The right way¶
The “right way” is not to disable the validation, but to specify the correct schema to be returned from an endpoint.
1from http import HTTPStatus
2
3import msgspec
4
5from dmr import APIError, Body, Controller, Headers, ResponseSpec, modify
6from dmr.errors import ErrorModel, ErrorType
7from dmr.plugins.msgspec import MsgspecSerializer
8
9
10class UserModel(msgspec.Struct):
11 email: str
12
13
14class HeaderModel(msgspec.Struct):
15 consumer: str = msgspec.field(name='X-API-Consumer')
16
17
18class UserController(Controller[MsgspecSerializer]):
19 @modify(
20 extra_responses=[
21 ResponseSpec(
22 ErrorModel,
23 status_code=HTTPStatus.PAYMENT_REQUIRED,
24 ),
25 ],
26 )
27 def post(
28 self,
29 parsed_body: Body[UserModel],
30 parsed_headers: Headers[HeaderModel],
31 ) -> UserModel:
32 if parsed_headers.consumer != 'my-api':
33 # Notice that this response is now documented in the spec,
34 # no error will happen, no need to disable the validation.
35 raise APIError(
36 self.format_error(
37 'Wrong API consumer',
38 error_type=ErrorType.user_msg,
39 ),
40 status_code=HTTPStatus.PAYMENT_REQUIRED,
41 )
42 # This response will be documented by default:
43 return parsed_body
44
Run result
$ curl http://127.0.0.1:8000/api/user/ -X POST -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json' -H 'X-API-Consumer: my-api'
{"email":"user@wms.org"}
$ curl http://127.0.0.1:8000/api/user/ -D - -X POST -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json' -H 'X-API-Consumer: not-my-api'
HTTP/1.1 402 Payment Required
date: Sun, 26 Apr 2026 21:12:33 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 59
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Wrong API consumer","type":"user_msg"}]}
And to disable the validation for production environment.
Example: https://github.com/wemake-services/wemake-django-template/blob/c003757fd33ba7dd1a9e7af7c3a175883d0c033b/%7B%7Bcookiecutter.project_name%7D%7D/server/settings/environments/production.py#L86