Describing response headers and cookies¶
Describing headers¶
You also must specify which headers are returned (if any).
When using “real endpoints”, you can provide headers parameter
to ResponseSpec
if there are headers you want to describe.
HeaderSpec is here to help.
You can create both required=True
(always must be present on the response object)
and required=False headers (might be missing in some cases):
1import uuid
2from http import HTTPStatus
3
4import pydantic
5from django.http import HttpResponse
6
7from dmr import Body, Controller, HeaderSpec, ResponseSpec, validate
8from dmr.plugins.pydantic import PydanticSerializer
9
10
11class UserModel(pydantic.BaseModel):
12 email: str
13
14
15class UserController(Controller[PydanticSerializer]):
16 @validate(
17 ResponseSpec(
18 UserModel,
19 status_code=HTTPStatus.OK,
20 headers={
21 'X-Created': HeaderSpec(),
22 'X-Our-Domain': HeaderSpec(required=False),
23 },
24 ),
25 )
26 def post(self, parsed_body: Body[UserModel]) -> HttpResponse:
27 uid = uuid.uuid4()
28 # This response would have an explicit status code `200`
29 # and one required header `X-Created` and one optional `X-Our-Domain`:
30 headers = {'X-Created': str(uid)}
31 if '@ourdomain.com' in parsed_body.email:
32 headers['X-Our-Domain'] = 'true'
33 return self.to_response(
34 parsed_body,
35 status_code=HTTPStatus.OK,
36 headers=headers,
37 )
38
Run result
$ curl http://127.0.0.1:8000/api/user/ -D - -X POST -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json'
HTTP/1.1 200 OK
date: Sun, 26 Apr 2026 21:12:22 GMT
server: uvicorn
X-Created: 59215763-83ff-444a-8344-2e92de2f563c
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 24
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"email":"user@wms.org"}
$ curl http://127.0.0.1:8000/api/user/ -D - -X POST -d '{"email": "user@ourdomain.com"}' -H 'Content-Type: application/json'
HTTP/1.1 200 OK
date: Sun, 26 Apr 2026 21:12:22 GMT
server: uvicorn
X-Created: 13c33369-ab42-4618-8afa-17bba9077bd4
X-Our-Domain: true
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 30
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"email":"user@ourdomain.com"}
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"
},
"UserModel": {
"properties": {
"email": {
"title": "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",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserModel"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserModel"
}
}
},
"description": "OK",
"headers": {
"X-Created": {
"required": true,
"schema": {
"type": "string"
}
},
"X-Our-Domain": {
"schema": {
"type": "string"
}
}
}
},
"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"
}
}
}
}
}
}
Note
All headers from the response objects are checked. We will report:
Required headers that exist in the spec, but not on the
responseAny headers that exist on the
response, but not present in the spec
Content-Type header is the only one that is always added automatically.
With “raw endpoints” you can also use
NewHeader marker which can set headers
with known values to the final response.
1from http import HTTPStatus
2
3import pydantic
4
5from dmr import Body, Controller, NewHeader, modify
6from dmr.plugins.pydantic import PydanticSerializer
7
8
9class UserModel(pydantic.BaseModel):
10 email: str
11
12
13class UserController(Controller[PydanticSerializer]):
14 @modify(
15 status_code=HTTPStatus.OK,
16 # Add explicit header:
17 headers={'X-Created': NewHeader(value='true')},
18 )
19 def post(self, parsed_body: Body[UserModel]) -> UserModel:
20 # This response would have an explicit status code `200`
21 # and new explicit header `{'X-Created': 'true'}`:
22 return parsed_body
23
Run result
$ curl http://127.0.0.1:8000/api/user/ -D - -X POST -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json'
HTTP/1.1 200 OK
date: Sun, 26 Apr 2026 21:12:23 GMT
server: uvicorn
Content-Type: application/json
X-Created: true
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 24
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"email":"user@wms.org"}
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"
},
"UserModel": {
"properties": {
"email": {
"title": "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",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserModel"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserModel"
}
}
},
"description": "OK",
"headers": {
"X-Created": {
"required": true,
"schema": {
"type": "string"
}
}
}
},
"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"
}
}
}
}
}
}
If you need headers with not static, but dynamic values, use “real endpoints”
and pass headers dict to
to_response() method.
The last important thing about headers
is skip_validation attribute.
It is used to describe headers that:
Will be set in the response by someone else outside the framework, like HTTP proxy or Django’s own middleware. See
django.contrib.sessions.middleware.SessionMiddlewareas a notable exampleWill be validated to be NOT present in the response from our framework. Since it is designed to be added later, it should not be already present
Important
Header definitions are case insensitive according to the HTTP spec.
Session and session is the same header.
Describing cookies¶
Warning
Some may say that returning cookies is not “RESTful”, because cookies is an implicit state, that RESTful APIs must not have. Be careful, only use this feature when you need to.
See: https://parottasalna.hashnode.dev/is-it-okay-to-add-cookie-to-a-rest-api
We also support setting and validating response cookies.
You can use NewCookie
to add new cookies with statically known values to “raw endpoints”.
Or CookieSpec with both types
of endpoints to describe response cookies.
1import pydantic
2
3from dmr import Body, Controller, NewCookie, modify
4from dmr.plugins.pydantic import PydanticSerializer
5
6
7class UserModel(pydantic.BaseModel):
8 email: str
9
10
11class UserController(Controller[PydanticSerializer]):
12 @modify(
13 # Add explicit cookie:
14 cookies={'user_created': NewCookie(value='true', max_age=1000)},
15 )
16 def post(self, parsed_body: Body[UserModel]) -> UserModel:
17 # This response would have an implicit status code `201`
18 # and explicit cookie `user_created` set to `true` with `max-age=1000`
19 return parsed_body
20
Run result
$ curl http://127.0.0.1:8000/api/user/ -D - -X POST -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json'
HTTP/1.1 201 Created
date: Sun, 26 Apr 2026 21:12:24 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 24
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
Set-Cookie: user_created=true; expires=Sun, 26 Apr 2026 21:29:04 GMT; Max-Age=1000; Path=/; SameSite=lax
{"email":"user@wms.org"}
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"
},
"UserModel": {
"properties": {
"email": {
"title": "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",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserModel"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserModel"
}
}
},
"description": "Created",
"headers": {
"Set-Cookie: user_created": {
"required": true,
"schema": {
"example": "user_created=123",
"type": "string"
}
}
}
},
"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 you can set any cookies to django.http.HttpResponse.cookies
with “real endpoints”. Since we have strict schemas,
it is required to describe the set cookies with
CookieSpec:
1import uuid
2from http import HTTPStatus
3
4import pydantic
5from django.http import HttpResponse
6
7from dmr import Body, Controller, CookieSpec, NewCookie, ResponseSpec, validate
8from dmr.plugins.pydantic import PydanticSerializer
9
10
11class UserModel(pydantic.BaseModel):
12 email: str
13
14
15class UserController(
16 Controller[PydanticSerializer],
17):
18 @validate(
19 ResponseSpec(
20 UserModel,
21 status_code=HTTPStatus.CREATED,
22 cookies={
23 'user_id': CookieSpec(),
24 'session': CookieSpec(max_age=1000, required=False),
25 },
26 ),
27 )
28 def post(self, parsed_body: Body[UserModel]) -> HttpResponse:
29 uid = uuid.uuid4()
30 # This response would have one required cookie `user_id`
31 # and one optional cookie `session`:
32 cookies = {'user_id': NewCookie(value=str(uid))}
33 if '@ourdomain.com' in parsed_body.email:
34 cookies['session'] = NewCookie(value='true', max_age=1000)
35 return self.to_response(
36 parsed_body,
37 cookies=cookies,
38 )
39
Run result
$ curl http://127.0.0.1:8000/api/user/ -D - -X POST -d '{"email": "user@ourdomain.com"}' -H 'Content-Type: application/json'
HTTP/1.1 201 Created
date: Sun, 26 Apr 2026 21:12:24 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 30
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
Set-Cookie: user_id=e11aff7e-8a72-4d9a-859c-4f92d9ad06fa; Path=/; SameSite=lax
Set-Cookie: session=true; expires=Sun, 26 Apr 2026 21:29:04 GMT; Max-Age=1000; Path=/; SameSite=lax
{"email":"user@ourdomain.com"}
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"
},
"UserModel": {
"properties": {
"email": {
"title": "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",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserModel"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserModel"
}
}
},
"description": "Created",
"headers": {
"Set-Cookie: session": {
"schema": {
"example": "session=123",
"type": "string"
}
},
"Set-Cookie: user_id": {
"required": true,
"schema": {
"example": "user_id=123",
"type": "string"
}
}
}
},
"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"
}
}
}
}
}
}
The last important thing about cookies
is skip_validation attribute.
It is used to describe cookies that:
Will be set in the response by someone else outside the framework, like HTTP proxy or Django’s own middleware. See
django.contrib.sessions.middleware.SessionMiddlewareas a notable exampleWill be validated to be NOT present in the response from our framework. Since it is designed to be added later, it should not be already present
Note
All cookie parts are validated by default. Except expires field,
because it is relative to the current time.
Important
Cookie definitions are case sensitive according to the HTTP spec.
Session and session are two different cookies.