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

  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.

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?

  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: Sun, 26 Apr 2026 21:11: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"
          }
        }
      }
    }
  }
}

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

  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.

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 0x7a3ebe2f3e20>]

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

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.