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:

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?

  1. We define a controller that uses regular self.kwargs dict with path params with no extra parsing from our side

  2. We define a custom ResponseSpec instance with 404 as a response code, Path injects this response automatically, but since we don’t use – we have to do that manually for our Response validation to work

  3. We also show how one can use APIError to raise custom 404 errors when some objects are not found

  4. We define an api url with django.urls.path() (or with django.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.

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?

  1. When you need typed path parameter model

  2. When they have more metadata than regular Django can provide. For example: only positive integers. Or str with an exact length

  3. When you only need self.kwargs to be parsed, because Path does not support variadic url args from self.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:

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"
          }
        }
      }
    }
  }
}

What happens in this example?

  1. We define a Path model using msgspec.Struct or pydantic.BaseModel. Other types are also supported: typing.TypedDict, dataclasses.dataclass(), etc

  2. Next, we use Path component, provide the model as a type parameter, and subclass it when defining Controller type

  3. Then we use self.parsed_path that will have the correct model type

What is the difference from the raw path() model?

  1. Path component automatically injects 404 error into the final schema, no need to manually defined 404 ResponseSpec

  2. It performs a second validation of the self.kwargs with new extra metadata from the Path model

  3. It adds self.parsed_path attribute

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#

See Customizing parameter.

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: ComponentParser

Parses 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/100 which will be translated into {'user_id': 100} into UserPath model.

Parameter for Path component must be named parsed_path.

It is way stricter than the original Django’s routing system. For example, django allows to such cases:

  • user_id is defined as int in the path('user/<int:user_id>')

  • user_id is defined as str in the view function: def get(self, request, user_id: str): ...

In django-modern-rest there’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 not EMPTY and 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.