Query parameters¶
You can define Query parameters
the same way you define Headers,
Path and
Cookies parameters.
Note
Parsed Query parameter must be named parsed_query.
This is how you can parse Query parameters:
1import msgspec
2
3from dmr import Controller, Query
4from dmr.plugins.msgspec import MsgspecSerializer
5
6
7class _QueryModel(msgspec.Struct):
8 query: str
9 count: int
10
11
12class ApiController(Controller[MsgspecSerializer]):
13 def get(self, parsed_query: Query[_QueryModel]) -> _QueryModel:
14 return parsed_query
15
Run result
$ curl 'http://127.0.0.1:8000/api/users/?query=abc&count=10' -X GET
{"query":"abc","count":10}
$ curl 'http://127.0.0.1:8000/api/users/?query=abc' -D - -X GET
HTTP/1.1 400 Bad Request
date: Thu, 09 Apr 2026 14:41:21 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 103
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Object missing required field `count` - at `$.parsed_query`","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"
},
"_QueryModel": {
"properties": {
"count": {
"type": "integer"
},
"query": {
"type": "string"
}
},
"required": [
"query",
"count"
],
"title": "_QueryModel",
"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": "query",
"name": "query",
"required": true,
"schema": {
"type": "string"
}
},
{
"deprecated": false,
"in": "query",
"name": "count",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_QueryModel"
}
}
},
"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, Query
4from dmr.plugins.pydantic import PydanticSerializer
5
6
7class _QueryModel(pydantic.BaseModel):
8 query: str
9 count: int
10
11
12class ApiController(Controller[PydanticSerializer]):
13 def get(self, parsed_query: Query[_QueryModel]) -> _QueryModel:
14 return parsed_query
15
Run result
$ curl 'http://127.0.0.1:8000/api/users/?query=abc&count=10' -X GET
{"query":"abc","count":10}
$ curl 'http://127.0.0.1:8000/api/users/?query=abc' -D - -X GET
HTTP/1.1 400 Bad Request
date: Thu, 09 Apr 2026 14:41:22 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 89
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Field required","loc":["parsed_query","count"],"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"
},
"_QueryModel": {
"properties": {
"count": {
"title": "Count",
"type": "integer"
},
"query": {
"title": "Query",
"type": "string"
}
},
"required": [
"query",
"count"
],
"title": "_QueryModel",
"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": "query",
"name": "query",
"required": true,
"schema": {
"title": "Query",
"type": "string"
}
},
{
"deprecated": false,
"in": "query",
"name": "count",
"required": true,
"schema": {
"title": "Count",
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_QueryModel"
}
}
},
"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
Querymodel usingmsgspec.Structorpydantic.BaseModel. Other types are also supported:typing.TypedDict,dataclasses.dataclass(), etcNext, we use
Querycomponent, provide the model as a type parameter, and subclass it when definingControllertypeThen we use
self.parsed_querythat will have the correct model type
Customizing OpenAPI metadata for Query¶
Forcing query params to be a list¶
Internally query parameters are represented
as django.utils.datastructures.MultiValueDict in Django.
It supports setting and getting several values for a single key.
Users can customize how they want their values:
as single values or as lists of values.
To do so, use __dmr_force_list__ optional attribute.
Set it to frozenset of field aliases that need to be lists.
All other values will be regular single values:
1from typing import ClassVar
2
3import msgspec
4
5from dmr import Controller, Query
6from dmr.plugins.msgspec import MsgspecSerializer
7
8
9class _QueryModel(msgspec.Struct):
10 __dmr_force_list__: ClassVar[frozenset[str]] = frozenset(('query',))
11
12 query: list[str]
13 regular: int
14
15
16class ApiController(Controller[MsgspecSerializer]):
17 def get(self, parsed_query: Query[_QueryModel]) -> _QueryModel:
18 return parsed_query
19
Run result
$ curl 'http://127.0.0.1:8000/api/users/?query=abc&query=xyz®ular=1®ular=2' -X GET
{"query":["abc","xyz"],"regular":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"
},
"_QueryModel": {
"properties": {
"query": {
"items": {
"type": "string"
},
"type": "array"
},
"regular": {
"type": "integer"
}
},
"required": [
"query",
"regular"
],
"title": "_QueryModel",
"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": "query",
"name": "query",
"required": true,
"schema": {
"items": {
"type": "string"
},
"type": "array"
}
},
{
"deprecated": false,
"in": "query",
"name": "regular",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_QueryModel"
}
}
},
"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_force_list__ value in any way,
it is up to users to set.
Casting nulls¶
Queries in Django cannot be None by default.
So, when some tools send 'null' as a way to represent None,
we need to handle that.
To do so, set the field aliases that should do
that into __dmr_cast_null__:
1from typing import ClassVar
2
3import msgspec
4
5from dmr import Controller, Query
6from dmr.plugins.msgspec import MsgspecSerializer
7
8
9class _QueryModel(msgspec.Struct):
10 __dmr_cast_null__: ClassVar[frozenset[str]] = frozenset(('query',))
11
12 query: str | None
13 regular: str
14
15
16class ApiController(Controller[MsgspecSerializer]):
17 def get(self, parsed_query: Query[_QueryModel]) -> _QueryModel:
18 return parsed_query
19
Run result
$ curl 'http://127.0.0.1:8000/api/users/?query=abc®ular=null' -X GET
{"query":"abc","regular":"null"}
$ curl 'http://127.0.0.1:8000/api/users/?query=null®ular=null' -X GET
{"query":null,"regular":"null"}
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"
},
"_QueryModel": {
"properties": {
"query": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"regular": {
"type": "string"
}
},
"required": [
"query",
"regular"
],
"title": "_QueryModel",
"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": "query",
"name": "query",
"required": true,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
{
"deprecated": false,
"in": "query",
"name": "regular",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_QueryModel"
}
}
},
"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_cast_null__ value in any way,
it is up to users to set.
API Reference¶
- dmr.components.Query¶
Annotated alias for parsing query parameters.
alias of
Annotated[_QueryT, <dmr.components.QueryComponent object at 0x79bd5b0d7680>]
- class dmr.components.QueryComponent[source]¶
Bases:
ComponentParserParses query params of the request.
For example:
>>> import pydantic >>> from dmr import Query, Controller >>> from dmr.plugins.pydantic import PydanticSerializer >>> class ProductQuery(pydantic.BaseModel): ... category: str ... reversed: bool >>> class ProductListController(Controller[PydanticSerializer]): ... def get(self, parsed_query: Query[ProductQuery]) -> str: ... return parsed_query.category
Will parse a request like
?category=cars&reversed=trueintoProductQuerymodel.Parameter for
Querycomponent must be namedparsed_query.- context_name: ClassVar[str] = 'parsed_query'¶
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) dict[str, 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.