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
6from django.urls import include
7
8from dmr import APIError, Controller
9from dmr.errors import ErrorType
10from dmr.metadata import ResponseSpec
11from dmr.openapi import build_schema
12from dmr.openapi.views import OpenAPIJsonView
13from dmr.plugins.pydantic import PydanticSerializer
14from dmr.routing import Router, path
15
16
17class _PostModel(pydantic.BaseModel):
18 user_id: int
19 post_id: uuid.UUID
20
21
22class PostController(Controller[PydanticSerializer]):
23 responses = (
24 ResponseSpec(
25 Controller.error_model,
26 status_code=HTTPStatus.NOT_FOUND,
27 ),
28 )
29
30 def get(self) -> _PostModel:
31 assert_type(self.kwargs, dict[str, Any])
32 if self.kwargs['user_id'] <= 0:
33 # Here we simulate some logical error, when object is not found:
34 raise APIError(
35 self.format_error(
36 'Page not found',
37 error_type=ErrorType.not_found,
38 ),
39 status_code=HTTPStatus.NOT_FOUND,
40 )
41 return _PostModel(
42 user_id=self.kwargs['user_id'],
43 post_id=self.kwargs['post_id'],
44 )
45
46
47router = Router(
48 'api/',
49 [
50 # We define a regular Django path:
51 path(
52 'user/<int:user_id>/post/<uuid:post_id>/',
53 PostController.as_view(),
54 name='user',
55 ),
56 ],
57)
58schema = build_schema(router)
59
60urlpatterns = [
61 # Register our router in the final url patterns:
62 path(router.prefix, include((router.urls, 'test_app'), namespace='api')),
63 # Add swagger:
64 path('docs/openapi.json/', OpenAPIJsonView.as_view(schema), name='openapi'),
65]
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Page not found at /api/user/1/post/wrong/</title>
<meta name="robots" content="NONE,NOARCHIVE">
<style>
html * { padding:0; margin:0; }
body * { padding:10px 20px; }
body * * { padding:0; }
body { font-family: sans-serif; background:#eee; color:#000; }
body > :where(header, main, footer) { border-bottom:1px solid #ddd; }
h1 { font-weight:normal; margin-bottom:.4em; }
h1 small { font-size:60%; color:#666; font-weight:normal; }
table { border:none; border-collapse: collapse; width:100%; }
td, th { vertical-align:top; padding:2px 3px; }
th { width:12em; text-align:right; color:#666; padding-right:.5em; }
#info { background:#f6f6f6; }
#info ol { margin: 0.5em 4em; }
#info ol li { font-family: monospace; }
#summary { background: #ffc; }
#explanation { background:#eee; border-bottom: 0px none; }
pre.exception_value { font-family: sans-serif; color: #575757; font-size: 1.5em; margin: 10px 0 10px 0; }
</style>
</head>
<body>
<header id="summary">
<h1>Page not found <small>(404)</small></h1>
<table class="meta">
<tr>
<th scope="row">Request Method:</th>
<td>GET</td>
</tr>
<tr>
<th scope="row">Request URL:</th>
<td>http://127.0.0.1:46635/api/user/1/post/wrong/</td>
</tr>
</table>
</header>
<main id="info">
<p>
Using the URLconf defined in <code>url_conf</code>,
Django tried these URL patterns, in this order:
</p>
<ol>
<li>
<code>
api/
</code>
<code>
user/<int:user_id>/post/<uuid:post_id>/
[name='user']
</code>
</li>
<li>
<code>
docs/openapi.json/
[name='openapi']
</code>
</li>
</ol>
<p>
The current path, <code>api/user/1/post/wrong/</code>,
didn’t match any of these.
</p>
</main>
<footer id="explanation">
<p>
You’re seeing this error because you have <code>DEBUG = True</code> in
your Django settings file. Change that to <code>False</code>, and Django
will display a standard 404 page.
</p>
</footer>
</body>
</html>
$ 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"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/user/{user_id}/post/{post_id}/": {
"get": {
"deprecated": false,
"operationId": "getPostcontrollerApiUserUserIdPostPostId",
"parameters": [
{
"deprecated": false,
"in": "path",
"name": "user_id",
"required": true,
"schema": {
"title": "User Id",
"type": "integer"
}
},
{
"deprecated": false,
"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.
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.
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 django.urls import include
5from typing_extensions import TypedDict
6
7from dmr import Controller, Path
8from dmr.openapi import build_schema
9from dmr.openapi.views import OpenAPIJsonView
10from dmr.plugins.msgspec import MsgspecSerializer
11from dmr.routing import Router, path
12
13
14class _PathModel(TypedDict):
15 user_id: Annotated[str, msgspec.Meta(min_length=4, max_length=4)]
16 post_id: Annotated[int, msgspec.Meta(gt=0)]
17
18
19class PostController(Controller[MsgspecSerializer]):
20 def get(self, parsed_path: Path[_PathModel]) -> _PathModel:
21 return parsed_path
22
23
24router = Router(
25 'api/',
26 [
27 # We define a regular Django path:
28 path(
29 'user/<str:user_id>/post/<int:post_id>/',
30 PostController.as_view(),
31 name='user',
32 ),
33 ],
34)
35schema = build_schema(router)
36
37urlpatterns = [
38 # Register our router in the final url patterns:
39 path(router.prefix, include((router.urls, 'test_app'), namespace='api')),
40 # Add swagger:
41 path('docs/openapi.json/', OpenAPIJsonView.as_view(schema), name='openapi'),
42]
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: Thu, 09 Apr 2026 14:41:19 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"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/user/{user_id}/post/{post_id}/": {
"get": {
"deprecated": false,
"operationId": "getPostcontrollerApiUserUserIdPostPostId",
"parameters": [
{
"deprecated": false,
"in": "path",
"name": "post_id",
"required": true,
"schema": {
"exclusiveMinimum": 0,
"type": "integer"
}
},
{
"deprecated": false,
"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 django.urls import include
5from typing_extensions import TypedDict
6
7from dmr import Controller, Path
8from dmr.openapi import build_schema
9from dmr.openapi.views import OpenAPIJsonView
10from dmr.plugins.pydantic import PydanticSerializer
11from dmr.routing import Router, path
12
13
14class _PathModel(TypedDict):
15 user_id: Annotated[str, pydantic.Field(min_length=4, max_length=4)]
16 post_id: Annotated[int, pydantic.Field(gt=0)]
17
18
19class PostController(Controller[PydanticSerializer]):
20 def get(self, parsed_path: Path[_PathModel]) -> _PathModel:
21 return parsed_path
22
23
24router = Router(
25 'api/',
26 [
27 # We define a regular Django path:
28 path(
29 'user/<str:user_id>/post/<int:post_id>/',
30 PostController.as_view(),
31 name='user',
32 ),
33 ],
34)
35schema = build_schema(router)
36
37urlpatterns = [
38 # Register our router in the final url patterns:
39 path(router.prefix, include((router.urls, 'test_app'), namespace='api')),
40 # Add swagger:
41 path('docs/openapi.json/', OpenAPIJsonView.as_view(schema), name='openapi'),
42]
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: Thu, 09 Apr 2026 14:41:20 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"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/user/{user_id}/post/{post_id}/": {
"get": {
"deprecated": false,
"operationId": "getPostcontrollerApiUserUserIdPostPostId",
"parameters": [
{
"deprecated": false,
"in": "path",
"name": "user_id",
"required": true,
"schema": {
"maxLength": 4,
"minLength": 4,
"title": "User Id",
"type": "string"
}
},
{
"deprecated": false,
"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 schemaIt 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.
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 0x79bd5a37efe0>]
- 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: 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.
- classmethod 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.