Components#

django-modern-rest utilizes component approach to parse all the unstructured things like headers, body, and cookies into a strongly typed and validated model.

To use a component, you can just add it as a parameter to your endpoint method inside a Controller.

How does it work?

  • In import time, when controller is first created, we iterate over all existing endpoints in this class

  • For each endpoint we fetch method annotations and find all ComponentParser objects, they will be treated as component parsers

  • Next, we create a request parsing model during the import time, with all combined fields to be parsed later

  • In runtime, when request is received, we provide the needed data for this single parsing model

  • If everything is ok, we call the needed endpoint with the correct data

  • If there’s a parsing error we raise RequestSerializationError and return a beautiful error message for the user

You can use existing ones or create your own.

Note

All existing components should only be inherited for parsing. If you want to change the implementation details of a component – create a new one from scratch.

You can still delegate parts of the work to existing ones.

What is inside a component?#

All components consist of two parts:

  1. The first one is a ComponentParser subclass, which knows how to provide the required data for itself, build OpenAPI schemas and etc. For example: QueryComponent

  2. The second part is a typing.Annotated based annotation that has a component parser instance as metadata. These annotations will be used by the end users. For example, Query

Note

Type aliases have first-class support: UserBody: TypeAlias = Body[User] and type UserBody = Body[User] both work as component annotations, including aliases of aliases and subscripted generic aliases like type Payload[ModelT] = Body[ModelT].

Default values#

Component parameters can have default values, just like any other Python parameters. When a request has no data for such a component, the endpoint receives its default value. Defaults are real Python defaults: they are passed as-is, without any copies. Regular parameters with defaults, like help_text: str = 'default', can be mixed with components.

Run result

$ curl http://127.0.0.1:8000/api/products/ -X POST
{"category":"all","page":1}

$ curl 'http://127.0.0.1:8000/api/products/?page=2' -X POST -d '{"category": "cars"}' -H 'Content-Type: application/json'
{"category":"cars","page":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"
      },
      "ProductFilters": {
        "properties": {
          "category": {
            "type": "string"
          }
        },
        "required": [
          "category"
        ],
        "title": "ProductFilters",
        "type": "object"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/productcontroller/": {
      "post": {
        "operationId": "postProductControllerApiProductcontroller",
        "parameters": [
          {
            "in": "query",
            "name": "page",
            "schema": {
              "default": 1,
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/ProductFilters"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "additionalProperties": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "integer"
                      }
                    ]
                  },
                  "type": "object"
                }
              }
            },
            "description": "Created"
          },
          "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 counts as “no data” for each component?

  • Body has no data when the request body is empty, for multipart/form-data requests: when there are no fields and no files

  • Query has no data when there are no query parameters

  • Headers has no data when there are no headers, which is practically never the case for real requests

  • Cookies has no data when there are no cookies

  • Path has no data when the url pattern has no parameters

  • FileMetadata has no data when no files were uploaded

Custom components receive the default value as the default parameter of provide_context_data() and decide when to return it.

When the request has some data, it is always validated against the model, so Body[Model | None] = None still returns an error for invalid bodies, it does not silently fall back to None.

Note

Defaults are passed to the model that parses the whole request, so the rules of the serializer’s model type apply: dataclasses.dataclass() for pydantic and msgspec.Struct for msgspec reject mutable defaults like [] and non-frozen model instances during the import time. Use None or frozen models as defaults.

Warning

The | None part must be inside the component annotation, so it must be Body[Model | None] = None and not Body[Model] | None = None.

The second form hides the component behind a union, so it would not be parsed at all. We raise UnsolvableAnnotationsError during the import time for any annotation that hides a component inside other types, like Body[Model] | None or list[Body[Model]].

Defaults are also reflected in the OpenAPI schema: a Body with a default is documented with required: false, and parameters of other components with defaults are documented as not required. Path parameters are always required by the OpenAPI specification.

Browse components#

Query

Parsing query parameters.

Query parameters
Headers

Parsing header parameters.

Header parameters
Cookies

Parsing cookie parameters.

Cookie parameters
Path

Parsing path parameters.

Path parameters
Body

Parsing request body.

Request body
Files

Uploading files.

Uploading files

API Reference#

class dmr.components.ComponentParser[source]#

Base abstract provider for request components.

conditional_types(model: TypeForm[Any], model_meta: tuple[Any, ...]) → Mapping[str, Any][source]#

Provide conditional parsing types based on content type.

Some components parser might define different input models based on the request’s content type.

This method must return a mapping of content_type to the model. If this component support this.

context_name: ClassVar[str]#

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.

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

abstractmethod provide_context_data(endpoint: Endpoint, controller: Controller[BaseSerializer], *, field_model: TypeForm[Any], default: Any = EMPTY) → Any | tuple[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.

For example, when parsing something, we always have an option to fail a parsing, if some request does not fit our model.

validate(controller_cls: type[Controller[BaseSerializer]], metadata: EndpointMetadata) → None[source]#

Validates that the component is correctly defined.

By default does nothing. Runs in import time.