Plugins

To be able to support multiple serializer models like pydantic and msgspec, we have a concept of a plugin.

There are several bundled ones, but you can write your own as well. To do that see our advanced Serialization guide.

As a user you are only interested in choosing the right plugin for the controller definition.

from dmr.plugins.msgspec import MsgspecSerializer

Customizing serializers

There are several things why you can possibly want to customize an existing serializer.

Support more data types

By default, serialize_hook() and deserialize_hook() support not that many types.

You can customize the serializer to know how to serializer / deserialize more types by extending it and customizing the method you need.

Customizing the serializer context

We use dmr.endpoint.SerializerContext type to deserialize all components from a single model, so it would be much faster than parsing each component separately.

This class can be customized for several reasons.

Change the default strictness

Tools like pydantic offer several useful type conversions in non-strict mode. For example, '1' can be parsed as 1 if strict mode is not enabled.

It is kinda useful for request bodies, where you don’t control the clients.

Here’s how we determine the default strictness for pydantic models:

  1. If strict_validation is not None, we return the serializer-level strictness

  2. Then pydantic looks at strict attribute in ConfigDict

  3. Then pydantic looks at strict attribute for individual Field() items

We recommend to change the strictness on a per-model basis, but if you want to, you can subclass the SerializerContext to be strict / non-strict and use it for all controllers.

Endpoint optimizers

Before actually serving any requests, during import-time, we try to optimize the future validation.

For example, pydantic.TypeAdapter takes time to be created. Why doing it on the first request, when we can do that during the import time?

Each serializer must provide a type, which must be a subclass of BaseEndpointOptimizer to optimize / pre-compile / create / cache things that it can.

Writing a custom plugin

Our API is flexible enough to potentially support any custom third-party serializers of your choice, like:

Follow the API of PydanticSerializer and MsgspecSerializer.

You would need to:

Pydantic plugin

PydanticFastSerializer

pydantic plugin contains one extra serializer optimized for json usage. Our regular API requires parsers and renderers to format the final response, so you can negotiate the request and response formats.

However, for cases when you only have json requests and responses (which is quite common), use PydanticFastSerializer.

Warning

It will ignore all parsers and serializers and use the pydantic own way to serialize and deserialize objects to json bytestring.

It will work from 3 up 10 times faster depending on the data then the common serializer.

Run result

$ curl http://127.0.0.1:8000/api/users/ -X PUT -d '{"username": "sobolevn", "age": 27}' -H 'Content-Type: application/json'
{"username":"sobolevn","age":27}

$ curl http://127.0.0.1:8000/api/users/ -D - -X PUT -d '{"username": "sobolevn"}' -H 'Content-Type: application/json'
HTTP/1.1 400 Bad Request
date: Sun, 26 Apr 2026 21:11:54 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 86
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{"detail":[{"msg":"Field required","loc":["parsed_body","age"],"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"
      },
      "_User": {
        "properties": {
          "age": {
            "title": "Age",
            "type": "integer"
          },
          "username": {
            "title": "Username",
            "type": "string"
          }
        },
        "required": [
          "username",
          "age"
        ],
        "title": "_User",
        "type": "object"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "put": {
        "deprecated": false,
        "operationId": "putUsercontrollerApiUsercontroller",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/_User"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/_User"
                }
              }
            },
            "description": "OK"
          },
          "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"
          }
        }
      }
    }
  }
}

No API changes are required to use it if you don’t use other request / response formats.

Serialization / deserialization flags

We have to special attributes to change how pydantic serializes data:

  1. to_json_kwargs for serialization purposes

  2. to_model_kwargs for deserialization purposes

By default these flags only pass {'by_alias': True} to support field aliases, when they are defined.

For example, when working with pydantic.types.Json, one can set round_trip to True (which is not passed by default, because it disables computed fields):

Run result

$ curl http://127.0.0.1:8000/api/users/ -X POST -d '{"username": "sobolevn", "settings": "{\"status\": \"active\"}"}' -H 'Content-Type: application/json'
{"username":"sobolevn","settings":"{\"status\":\"active\"}"}

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"
      },
      "_User": {
        "properties": {
          "settings": {
            "contentMediaType": "application/json",
            "contentSchema": {
              "additionalProperties": {
                "type": "string"
              },
              "type": "object"
            },
            "title": "Settings",
            "type": "string"
          },
          "username": {
            "title": "Username",
            "type": "string"
          }
        },
        "required": [
          "username",
          "settings"
        ],
        "title": "_User",
        "type": "object"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "post": {
        "deprecated": false,
        "operationId": "postUsercontrollerApiUsercontroller",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/_User"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/_User"
                }
              }
            },
            "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"
          }
        }
      }
    }
  }
}

Msgspec plugin

attrs support

We support attrs.define() via msgspec compatibility layer. It has its own limitations. See msgspec docs.

Native support of attrs can be implemented in the future with its own serializer.