Header parameters¶
You can define Headers parameters
the same way you define Query,
Path and
Cookies parameters.
Note
Parsed Header parameter must be named parsed_headers.
Since most headers use - to separate words, but a variable
like cache-control is not a valid variable name in Python.
So, you would have to use aliases for field names.
Remember, that headers are also case insensitive:
1import msgspec
2
3from dmr import Controller, Headers
4from dmr.plugins.msgspec import MsgspecSerializer
5
6
7class _HeadersModel(msgspec.Struct):
8 cache: str = msgspec.field(name='cache-control')
9 client_id: int = msgspec.field(name='X-Client-Id', default=-1)
10
11
12class ApiController(Controller[MsgspecSerializer]):
13 def get(self, parsed_headers: Headers[_HeadersModel]) -> _HeadersModel:
14 return parsed_headers
15
Run result
$ curl http://127.0.0.1:8000/api/users/ -X GET -H 'Cache-Control: max-age=0'
{"cache-control":"max-age=0","X-Client-Id":-1}
$ curl http://127.0.0.1:8000/api/users/ -D - -X GET -H 'Cache-Control: max-age=0' -H 'X-Client-Id: wrong'
HTTP/1.1 400 Bad Request
date: Thu, 07 May 2026 12:57:35 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 105
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Expected `int`, got `str` - at `$.parsed_headers.X-Client-Id`","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"
},
"_HeadersModel": {
"properties": {
"X-Client-Id": {
"default": -1,
"type": "integer"
},
"cache-control": {
"type": "string"
}
},
"required": [
"cache-control"
],
"title": "_HeadersModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/apicontroller/": {
"get": {
"deprecated": false,
"operationId": "getApicontrollerApiApicontroller",
"parameters": [
{
"deprecated": false,
"in": "header",
"name": "cache-control",
"required": true,
"schema": {
"type": "string"
}
},
{
"deprecated": false,
"in": "header",
"name": "X-Client-Id",
"schema": {
"default": -1,
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_HeadersModel"
}
}
},
"description": "OK"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
1import pydantic
2
3from dmr import Controller, Headers
4from dmr.plugins.pydantic import PydanticSerializer
5
6
7class _HeadersModel(pydantic.BaseModel):
8 cache: str = pydantic.Field(alias='cache-control')
9 client_id: int = pydantic.Field(alias='X-Client-Id', default=-1)
10
11
12class ApiController(Controller[PydanticSerializer]):
13 def get(self, parsed_headers: Headers[_HeadersModel]) -> _HeadersModel:
14 return parsed_headers
15
Run result
$ curl http://127.0.0.1:8000/api/users/ -X GET -H 'Cache-Control: max-age=0'
{"cache-control":"max-age=0","X-Client-Id":-1}
$ curl http://127.0.0.1:8000/api/users/ -D - -X GET -H 'Cache-Control: max-age=0' -H 'X-Client-Id: wrong'
HTTP/1.1 400 Bad Request
date: Thu, 07 May 2026 12:57:36 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 152
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Input should be a valid integer, unable to parse string as an integer","loc":["parsed_headers","X-Client-Id"],"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"
}
]
},
"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"
},
"_HeadersModel": {
"properties": {
"X-Client-Id": {
"default": -1,
"title": "X-Client-Id",
"type": "integer"
},
"cache-control": {
"title": "Cache-Control",
"type": "string"
}
},
"required": [
"cache-control"
],
"title": "_HeadersModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/apicontroller/": {
"get": {
"deprecated": false,
"operationId": "getApicontrollerApiApicontroller",
"parameters": [
{
"deprecated": false,
"in": "header",
"name": "cache-control",
"required": true,
"schema": {
"title": "Cache-Control",
"type": "string"
}
},
{
"deprecated": false,
"in": "header",
"name": "X-Client-Id",
"schema": {
"default": -1,
"title": "X-Client-Id",
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_HeadersModel"
}
}
},
"description": "OK"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
What happens in this example?
We define a
Headersmodel usingmsgspec.Structorpydantic.BaseModel. Other types are also supported:typing.TypedDict,dataclasses.dataclass(), etcNext, we use
Headerscomponent, provide the model as a type parameter, and subclass it when definingControllertypeThen we use
self.parsed_headersthat will have the correct model type
Duplicated headers¶
By default Django joins several headers into a single value. This is a limitation of a WSGI protocol. But, even ASGI workers are forced to do the same in Django for compatibility.
X-Tag: a
X-Tag: b
Would become:
{'X-Tag': 'a,b'}
To force X-Tag to be a list you can use __dmr_split_commas__.
Specify lower-case header field aliases
which needs to be split by a ',' char:
1from typing import ClassVar
2
3import msgspec
4
5from dmr import Controller, Headers
6from dmr.plugins.msgspec import MsgspecSerializer
7
8
9class _HeadersModel(msgspec.Struct):
10 __dmr_split_commas__: ClassVar[frozenset[str]] = frozenset(('x-tag',))
11
12 tags: list[int] = msgspec.field(name='X-Tag')
13
14
15class UserController(Controller[MsgspecSerializer]):
16 def get(self, parsed_headers: Headers[_HeadersModel]) -> _HeadersModel:
17 return parsed_headers
18
Run result
$ curl http://127.0.0.1:8000/api/users/ -X GET -H 'X-Tag: 1' -H 'X-Tag: 2'
{"X-Tag":[1,2]}
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"
},
"_HeadersModel": {
"properties": {
"X-Tag": {
"items": {
"type": "integer"
},
"type": "array"
}
},
"required": [
"X-Tag"
],
"title": "_HeadersModel",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/usercontroller/": {
"get": {
"deprecated": false,
"operationId": "getUsercontrollerApiUsercontroller",
"parameters": [
{
"deprecated": false,
"in": "header",
"name": "X-Tag",
"required": true,
"schema": {
"items": {
"type": "integer"
},
"type": "array"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_HeadersModel"
}
}
},
"description": "OK"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
We don’t infer __dmr_split_commas__ value in any way,
it is up to users to set.
Danger
Some headers like
Accept: text/html, application/json
Cache-Control: no-cache, no-store
can naturally contain values with the ',' char.
If you split them, you might get a messed up value.
Customizing OpenAPI metadata for Headers¶
API Reference¶
- dmr.components.Headers¶
Annotated alias for parsing header parameters.
alias of
Annotated[_HeadersT, <dmr.components.HeadersComponent object at 0x70cd635568c0>]
- class dmr.components.HeadersComponent[source]¶
Bases:
ComponentParserParses request headers.
For example:
>>> import pydantic >>> from dmr import Headers, Controller >>> from dmr.plugins.pydantic import PydanticSerializer >>> class AuthHeaders(pydantic.BaseModel): ... token: str = pydantic.Field(alias='X-API-Token') >>> class UserCreateController(Controller[PydanticSerializer]): ... def get(self, parsed_headers: Headers[AuthHeaders]) -> str: ... return parsed_headers.token
Will parse request headers like
Token: secretintoAuthHeadersmodel.Parameter for
Headerscomponent must be namedparsed_headers.- context_name: ClassVar[str] = 'parsed_headers'¶
All subtypes must provide a unique name that will be used to parse context.
We use a single context for all parsing, this component will live under a dict field with this name.
- get_schema(model: Any, model_meta: tuple[Any, ...], metadata: EndpointMetadata, serializer: type[BaseSerializer], context: OpenAPIContext) list[Parameter | Reference] | RequestBody[source]¶
Generate OpenAPI spec for component.
- provide_context_data(endpoint: Endpoint, controller: Controller[BaseSerializer], *, field_model: Any) Any[source]¶
Return unstructured raw values for
serializer.from_python().It must return the same number of elements that has type vars. Basically, each type var is a model. Each element in a tuple is the corresponding data for that model.
When this method returns not a tuple and there’s only one type variable, it also works.