Path parameters#
Using native Django path params#
You don’t have to use Path to parse url parameters.
By default Django puts all url parameters
into self.args and self.kwargs.
Let’s take a look at the full example:
1import uuid
2from http import HTTPStatus
3from typing import Any, assert_type
4
5import pydantic
6
7from dmr import APIError, Controller
8from dmr.errors import ErrorType
9from dmr.metadata import ResponseSpec
10from dmr.openapi import build_schema
11from dmr.openapi.views import OpenAPIJsonView
12from dmr.plugins.pydantic import PydanticFastSerializer
13from dmr.routing import Router, path
14
15
16class _PostModel(pydantic.BaseModel):
17 user_id: int
18 post_id: uuid.UUID
19
20
21class PostController(Controller[PydanticFastSerializer]):
22 responses = (
23 ResponseSpec(
24 Controller.error_model,
25 status_code=HTTPStatus.NOT_FOUND,
26 ),
27 )
28
29 def get(self) -> _PostModel:
30 assert_type(self.kwargs, dict[str, Any])
31 if self.kwargs['user_id'] <= 0:
32 # Here we simulate some logical error, when object is not found:
33 raise APIError(
34 self.format_error(
35 'Page not found',
36 error_type=ErrorType.not_found,
37 ),
38 status_code=HTTPStatus.NOT_FOUND,
39 )
40 return _PostModel(
41 user_id=self.kwargs['user_id'],
42 post_id=self.kwargs['post_id'],
43 )
44
45
46router = Router(
47 'api/',
48 [
49 # We define a regular Django path:
50 path(
51 'user/<int:user_id>/post/<uuid:post_id>/',
52 PostController.as_view(),
53 name='user',
54 ),
55 ],
56)
57schema = build_schema(router)
58
59urlpatterns = [
60 # Register our router in the final url patterns:
61 router.to_urlpatterns(namespace='api'),
62 # Add swagger:
63 path('docs/openapi.json/', OpenAPIJsonView.as_view(schema), name='openapi'),
64]
Run result
$ curl http://127.0.0.1:8000/api/user/1/post/8b36dfc2-f168-47db-827a-7ae323539936/ -X GET
{"user_id":1,"post_id":"8b36dfc2-f168-47db-827a-7ae323539936"}
$ curl http://127.0.0.1:8000/api/user/1/post/wrong/ -X GET
{"detail":[{"msg":"Page not found","type":"not_found"}]}
$ curl http://127.0.0.1:8000/api/user/0/post/8b36dfc2-f168-47db-827a-7ae323539936/ -X GET
{"detail":[{"msg":"Page not found","type":"not_found"}]}
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"
},
"_PostModel": {
"properties": {
"post_id": {
"format": "uuid",
"title": "Post Id",
"type": "string"
},
"user_id": {
"title": "User Id",
"type": "integer"
}
},
"required": [
"user_id",
"post_id"
],
"title": "_PostModel",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/user/{user_id}/post/{post_id}/": {
"get": {
"operationId": "getPostControllerApiUserUserIdPostPostId",
"parameters": [
{
"in": "path",
"name": "user_id",
"required": true,
"schema": {
"title": "User Id",
"type": "integer"
}
},
{
"in": "path",
"name": "post_id",
"required": true,
"schema": {
"format": "uuid",
"title": "Post Id",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_PostModel"
}
}
},
"description": "OK"
},
"404": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Not Found"
},
"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 here?
We define a controller that uses regular
self.kwargsdict with path params with no extra parsing from our sideWe define a custom
ResponseSpecinstance with404as a response code,Pathinjects this response automatically, but since we don’t use – we have to do that manually for our Response validation to workWe also show how one can use
APIErrorto raise custom404errors when some objects are not foundWe define an api url with
django.urls.path()(or withdjango.urls.re_path()) and a common Django syntax for path parameters:'user/<int:user_id>/post/<uuid:post_id>/'
Django supports multiple pre-defined path converter types:
int, uuid, str, slug, path.
We have first-class support for all of them, showing all the possible metadata we possibly can for these converters.
See also
The main downside of this method is that self.kwargs is typed
as dict[str, Any]. Which is not always ideal.
If you need typed path parameters,
use Path component with a model.
Note
If you are using custom URL converters
and django.urls.register_converter(),
we won’t know your url parameter schema type in advance.
We default to str type for all url converters.
However, if you are using a different converter schema type,
you can use set __dmr_converter_schema__ attribute
with the specific type that you need in the schema.
It can be any annotation that is supported by the controller’s serializer
that is using this URL.
Setting __dmr_converter_schema__
to dmr.openapi.generators.component_parsers.ConverterSchema
will allow you to add pattern or description metadata to the schema.
Note
With django.urls.re_path(), url parameters are always typed
as str, because we cannot infer any better type from a regex.
But, we do copy the sub-pattern of each named group into the schema,
so r'^v(?P<version>\d+)/$' documents version
as {'type': 'string', 'pattern': '^(?:\d+)$'}.
Sub-patterns are copied as-is and only wrapped into anchors, we don’t translate them. JSON Schema requires ECMA-262 regexes, so Python-only syntax might not be supported by all OpenAPI tools.
Using Path component and parsing models#
When do you need to parse path parameters into models?
When you need typed path parameter model
When they have more metadata than regular Django can provide. For example: only positive integers. Or
strwith an exact lengthWhen you only need
self.kwargsto be parsed, becausePathdoes not support variadic url args fromself.args
You can define Path parameters
the same way you define Headers,
Query and
Cookies parameters.
Note
Parsed Path parameter must be named parsed_path.
This is how you can parse Path parameters into a model:
1from typing import Annotated
2
3import msgspec
4from typing_extensions import TypedDict
5
6from dmr import Controller, Path
7from dmr.openapi import build_schema
8from dmr.openapi.views import OpenAPIJsonView
9from dmr.plugins.msgspec import MsgspecSerializer
10from dmr.routing import Router, path
11
12
13class _PathModel(TypedDict):
14 user_id: Annotated[str, msgspec.Meta(min_length=4, max_length=4)]
15 post_id: Annotated[int, msgspec.Meta(gt=0)]
16
17
18class PostController(Controller[MsgspecSerializer]):
19 def get(self, parsed_path: Path[_PathModel]) -> _PathModel:
20 return parsed_path
21
22
23router = Router(
24 'api/',
25 [
26 # We define a regular Django path:
27 path(
28 'user/<str:user_id>/post/<int:post_id>/',
29 PostController.as_view(),
30 name='user',
31 ),
32 ],
33)
34schema = build_schema(router)
35
36urlpatterns = [
37 # Register our router in the final url patterns:
38 router.to_urlpatterns(namespace='api'),
39 # Add swagger:
40 path('docs/openapi.json/', OpenAPIJsonView.as_view(schema), name='openapi'),
41]
Run result
$ curl http://127.0.0.1:8000/api/user/abcd/post/1/ -X GET
{"user_id":"abcd","post_id":1}
$ curl http://127.0.0.1:8000/api/user/abcd/post/0/ -D - -X GET
HTTP/1.1 400 Bad Request
date: Mon, 05 Oct 2026 11:16:59 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 92
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Expected `int` >= 1 - at `$.parsed_path.post_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"
},
"_PathModel": {
"properties": {
"post_id": {
"exclusiveMinimum": 0,
"type": "integer"
},
"user_id": {
"maxLength": 4,
"minLength": 4,
"type": "string"
}
},
"required": [
"post_id",
"user_id"
],
"title": "_PathModel",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/user/{user_id}/post/{post_id}/": {
"get": {
"operationId": "getPostControllerApiUserUserIdPostPostId",
"parameters": [
{
"in": "path",
"name": "post_id",
"required": true,
"schema": {
"exclusiveMinimum": 0,
"type": "integer"
}
},
{
"in": "path",
"name": "user_id",
"required": true,
"schema": {
"maxLength": 4,
"minLength": 4,
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_PathModel"
}
}
},
"description": "OK"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"404": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when path parameters do not match"
},
"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 typing import Annotated
2
3import pydantic
4from typing_extensions import TypedDict
5
6from dmr import Controller, Path
7from dmr.openapi import build_schema
8from dmr.openapi.views import OpenAPIJsonView
9from dmr.plugins.pydantic import PydanticFastSerializer
10from dmr.routing import Router, path
11
12
13class _PathModel(TypedDict):
14 user_id: Annotated[str, pydantic.Field(min_length=4, max_length=4)]
15 post_id: Annotated[int, pydantic.Field(gt=0)]
16
17
18class PostController(Controller[PydanticFastSerializer]):
19 def get(self, parsed_path: Path[_PathModel]) -> _PathModel:
20 return parsed_path
21
22
23router = Router(
24 'api/',
25 [
26 # We define a regular Django path:
27 path(
28 'user/<str:user_id>/post/<int:post_id>/',
29 PostController.as_view(),
30 name='user',
31 ),
32 ],
33)
34schema = build_schema(router)
35
36urlpatterns = [
37 # Register our router in the final url patterns:
38 router.to_urlpatterns(namespace='api'),
39 # Add swagger:
40 path('docs/openapi.json/', OpenAPIJsonView.as_view(schema), name='openapi'),
41]
Run result
$ curl http://127.0.0.1:8000/api/user/abcd/post/1/ -X GET
{"user_id":"abcd","post_id":1}
$ curl http://127.0.0.1:8000/api/user/abcd/post/0/ -D - -X GET
HTTP/1.1 400 Bad Request
date: Mon, 05 Oct 2026 11:17:01 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 106
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"Input should be greater than 0","loc":["parsed_path","post_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"
},
"_PathModel": {
"properties": {
"post_id": {
"exclusiveMinimum": 0,
"title": "Post Id",
"type": "integer"
},
"user_id": {
"maxLength": 4,
"minLength": 4,
"title": "User Id",
"type": "string"
}
},
"required": [
"user_id",
"post_id"
],
"title": "_PathModel",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/user/{user_id}/post/{post_id}/": {
"get": {
"operationId": "getPostControllerApiUserUserIdPostPostId",
"parameters": [
{
"in": "path",
"name": "user_id",
"required": true,
"schema": {
"maxLength": 4,
"minLength": 4,
"title": "User Id",
"type": "string"
}
},
{
"in": "path",
"name": "post_id",
"required": true,
"schema": {
"exclusiveMinimum": 0,
"title": "Post Id",
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_PathModel"
}
}
},
"description": "OK"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"404": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when path parameters do not match"
},
"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
Pathmodel usingmsgspec.Structorpydantic.BaseModel. Other types are also supported:typing.TypedDict,dataclasses.dataclass(), etcNext, we use
Pathcomponent, provide the model as a type parameter, and subclass it when definingControllertypeThen we use
self.parsed_paththat will have the correct model type
What is the difference from the raw path() model?
Pathcomponent automatically injects404error into the final schema, no need to manually defined404ResponseSpecIt performs a second validation of the
self.kwargswith new extra metadata from thePathmodelIt adds
self.parsed_pathattribute
Important
Make sure that your path() URL pattern and Path model fields match.
We don’t automatically validate it.
Note
Path model fields can have default values.
Frameworks like FastAPI route each view with a single @app.get(url),
so path parameters with defaults make no sense there.
But a single controller can be routed to several URLs,
and these URLs can differ in a single URL parameter:
urlpatterns = [
path('users/', UserController.as_view()),
path('users/<int:user_id>/', UserController.as_view()),
]
A default value, like user_id: int | None = None,
makes the same Path model work for both URLs.
OpenAPI requires all path parameters to be required,
so they always have required: true in the schema,
even when they have default values.
Customizing OpenAPI metadata for Path#
API Reference#
- dmr.components.Path#
Annotated alias for parsing path parameters.
alias of
Annotated[_PathT, <dmr.components.PathComponent object at 0x7170c1d08200>]
- class dmr.components.PathComponent[source]#
Bases:
ComponentParserParses the url part of the request.
For example:
>>> import pydantic >>> from dmr import Path, Controller >>> from dmr.routing import Router >>> from dmr.plugins.pydantic import PydanticSerializer >>> from django.urls import include, path >>> class UserPath(pydantic.BaseModel): ... user_id: int >>> class UserUpdateController(Controller[PydanticSerializer]): ... def get(self, parsed_path: Path[UserPath]) -> int: ... return parsed_path.user_id >>> router = Router( ... 'api/', ... [ ... path( ... 'user/<int:user_id>', ... UserUpdateController.as_view(), ... name='users', ... ), ... ], ... ) >>> urlpatterns = [ ... path( ... router.prefix, ... include((router.urls, 'rest_app'), namespace='api'), ... ), ... ]
Will parse a url path like
/user_id/100which will be translated into{'user_id': 100}intoUserPathmodel.Parameter for
Pathcomponent must be namedparsed_path.It is way stricter than the original Django’s routing system. For example, django allows to such cases:
user_idis defined asintin thepath('user/<int:user_id>')user_idis defined asstrin the view function:def get(self, request, user_id: str): ...
In
django-modern-restthere’s now a way to validate this in runtime.- context_name: ClassVar[str] = 'parsed_path'#
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: TypeForm[Any], model_meta: tuple[Any, ...], metadata: EndpointMetadata, controller_cls: type[Controller[BaseSerializer]], context: OpenAPIContext) list[Parameter | Reference] | RequestBody[source]#
Generate OpenAPI spec for component.
Changed in version 0.16.0: serializer parameter was changed to be controller_cls.
- provide_context_data(endpoint: Endpoint, controller: Controller[BaseSerializer], *, field_model: TypeForm[Any], default: Any = EMPTY) 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.
default is the default value of the endpoint parameter, like
parsed_body: Body[Model | None] = None. When it is notEMPTYand the request has no data for this component, return default as-is.Changed in version 0.16.0: Added default parameter.
- provide_response_specs(metadata: EndpointMetadata, controller_cls: type[Controller[BaseSerializer]], existing_responses: Mapping[HTTPStatus, ResponseSpec]) list[ResponseSpec][source]#
Return a list of extra responses that this component produces.
Path component implies that we are looking for something. So, it is natural to have 404 in the specification.