Skip to main content


django-dto-field logo

Tests Typing and Linters PyPI version
wemake style crashed-pixels Monthly PyPI downloads

For Django developers who want to store structures in model fields but do not want to manually serialize dictionaries into DTO objects and vice versa. django-dto-field hide converting logic and gives you original Django interface.

Table of contents

  1. Quick start
  2. No schema is set
  3. Supported DTO types
    1. User-defined adapters
  4. Django Fields
    1. DTOField
    2. DTOBinaryField
  5. Supported versions and databases
  6. Help and contribute
  7. License

Quick start

Django itself have a JSONField for storing structures in JSON comparable format. But it have some restrictions. Let's see it through comparing JSONField and DTOField on one task.

First, install the package:

pip install django-dto-field

Suppose you have these nested dataclass DTOs:

# app/my_dto.py

from dataclasses import dataclass

@dataclass
class Address:
    city: str

@dataclass
class Customer:
    name: str
    address: Address

To store a customer in a Django model, let's compare Django's built-in JSONField with django-dto-field's DTOField:

# app/models.py

from django.db.models import JSONField, Model
from django_dto_field.fields.json import DTOField
from app.my_dto import Customer

class DjangoOrder(Model):
    customer = JSONField()

class DTOOrder(Model):
    customer = DTOField(schema=Customer)

Add app to INSTALLED_APPS, create and apply migrations, then open the Django shell with ./manage.py shell.

Create a customer to store:

>>> from app.my_dto import Address, Customer
>>> customer = Customer(name="Alice", address=Address(city="London"))

Let's try to store and load it with JSONField.

>>> from dataclasses import asdict
>>> from app.models import DjangoOrder

>>> # JSONField requires a JSON-compatible value, so convert the dataclass first.
>>> order = DjangoOrder.objects.create(customer=asdict(customer))
>>> order.refresh_from_db()

>>> # Loading returns dictionaries, including nested values.
>>> assert isinstance(order.customer, dict)
>>> assert isinstance(order.customer["address"], dict)
>>> assert order.customer["address"]["city"] == "London"

>>> # Standard Django JSON lookups work.
>>> DjangoOrder.objects.filter(customer__address__city="London")

>>> # Reconstruct the nested dataclasses manually to restore attribute access.
>>> payload = order.customer
>>> customer = Customer(
...     name=payload["name"],
...     address=Address(**payload["address"]),
... )

>>> # JSONField does not enforce the dataclass schema.
>>> invalid_customer = {
...     "name": "Alice",
...     "address": {"city": 123},  # city should be a string
... }

>>> # Valid JSON is accepted even though city has the wrong type.
>>> DjangoOrder.objects.create(customer=invalid_customer)

Now let's do the same with django-dto-field.

>>> from app.models import DTOOrder

>>> # DTOField handles dataclass conversion automatically.
>>> order = DTOOrder.objects.create(customer=customer)
>>> order.refresh_from_db()

>>> # Loading automatically reconstructs the nested dataclasses.
>>> assert isinstance(order.customer, Customer)
>>> assert isinstance(order.customer.address, Address)
>>> assert order.customer.address.city == "London"

>>> # The same Django JSON lookups work.
>>> DTOOrder.objects.filter(customer__address__city="London")

>>> # Invalid data raises a Django ValidationError during model construction by default.
>>> from django.core.exceptions import ValidationError
>>> invalid_customer = {
...     "name": "Alice",
...     "address": {"city": 123},  # city should be a string
... }

>>> try:
...     # DTOField also accepts dictionaries and validates them against the dataclass schema.
...     DTOOrder.objects.create(customer=invalid_customer)
... except ValidationError:
...     print("The customer data does not match the schema!")

As a result, django-dto-field provides:

  • Familiar Django model and query interfaces;
  • Less conversion boilerplate, so you can focus on business logic;
  • Runtime schema validation to catch invalid data early.

When might you prefer Django's JSONField? If your data structure changes frequently and you do not need typed objects or schema validation, plain dictionaries may be simpler than maintaining a schema.

No schema is set

Omit schema on any DTO field to store dictionaries with JSON-compatible values. Loading returns a dict. Add the Event model below to app/models.py, create and apply migrations, then run the database operations in the Django shell.

>>> from django.db import models
>>> from django_dto_field.fields.json import DTOField
>>> class Event(models.Model):
...     details = DTOField(default=dict)

>>> from app.models import Event
>>> event = Event.objects.create(details={"kind": "signup"})
>>> event.refresh_from_db()
>>> event.details
{'kind': 'signup'}

Supported DTO types

The default adapter supports dataclasses and dictionaries. Dataclasses require an explicit, importable schema class. Dictionaries have no declared member schema, but the field still checks that they can be converted to JSON-compatible data.

If you would like support for another DTO type, please open an issue in the issue tracker. You can also provide an importable adapter= class implementing DTOAdapter from django_dto_field.adapters.base. Pull requests are welcome!

User-defined adapters

A custom adapter allows you to use DTO types beyond those supported by the default MsgspecAdapter, which handles dictionaries and dataclasses. It defines how DTO instances are recognized, converted into JSON-compatible dictionaries, and reconstructed from stored data.

For example, an application using Pydantic models can provide a custom adapter to use Pydantic's validation and serialization features.

A custom adapter must implement the DTOAdapter protocol:

  • __init__(schema): Configure the adapter for the supplied schema class. Raise SchemaError if the schema is unsupported.
  • is_instance(instance): Check whether an object matches the expected DTO type.
  • to_data(instance): Convert a DTO instance into a dictionary containing JSON-compatible values. Raise SerializationError if serialization fails.
  • from_data(payload): Validate dictionary data and reconstruct a DTO instance. Raise DTOValidationError if validation fails.

The schema class does not have to be the same class as the DTO instance returned by the adapter. Pass the adapter class through the adapter= argument of DTOField, together with the appropriate schema=.

Advantages

  • Supports third-party DTO libraries and custom data models.
  • Allows application-specific validation and conversion.
  • Integrates existing DTO implementations.

Limitations

  • The adapter must implement the expected interface correctly.
  • Custom validation and conversion logic must be maintained.
  • The adapter must handle errors and return JSON-compatible dictionary data.

Example

The following example demonstrates a custom adapter for a simple user-defined DTO:

from typing import Any, TypeGuard

from django_dto_field.adapters.base import DTOAdapter
from django_dto_field.exceptions.base import (
    DTOValidationError,
    SchemaError,
    SerializationError,
)


class UserDTO:
    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age


class UserAdapter(DTOAdapter[UserDTO]):
    def __init__(self, schema: type[Any]) -> None:
        if schema is not UserDTO:
            raise SchemaError("Unsupported schema.")
        self.schema = schema

    def is_instance(self, instance: object) -> TypeGuard[UserDTO]:
        return isinstance(instance, self.schema)

    def to_data(self, instance: UserDTO) -> dict[str, Any]:
        try:
            name = instance.name
            age = instance.age
        except AttributeError as error:
            raise SerializationError(
                "Could not serialize UserDTO."
            ) from error

        if not isinstance(name, str) or type(age) is not int:
            raise SerializationError("Invalid UserDTO attributes.")

        return {"name": name, "age": age}

    def from_data(self, payload: dict[str, Any]) -> UserDTO:
        name = payload.get("name")
        age = payload.get("age")

        if not isinstance(name, str) or type(age) is not int:
            raise DTOValidationError("Invalid user data.")

        return self.schema(name=name, age=age)

Use the custom adapter in a Django model by passing it to DTOField alongside the schema:

from django.db import models
from django_dto_field.fields.json import DTOField


class UserRecord(models.Model):
    user = DTOField(
        schema=UserDTO,
        adapter=UserAdapter,
    )

In this example, to_data() converts a UserDTO instance into a JSON-compatible dictionary, while from_data() validates stored data and reconstructs the DTO. The adapter raises the appropriate project-specific exceptions for unsupported schemas, invalid attributes, and invalid input data.

Django Fields

The library wraps Django's JSON and binary model fields to add DTO conversion and validation. Each field accepts its Django base field's options, such as null and blank.

Schema validation runs on assignment by default, including model construction. Set DTOField(schema=Customer, validate_on_assignment=False) on a field to defer validation until conversion or saving. In-place changes to a DTO or dictionary are checked on save. SQL expressions bypass literal-value validation. DTO results are checked on load. Call full_clean() to run Django field validators; they receive the stored mapping or bytes.

For optional form input, use null=True, blank=True. With the default dictionary schema and blank=True, submit {} to store an empty dictionary; blank=True alone does not allow SQL NULL.

DTOField

DTOField wraps Django's JSONField to store DTOs as native JSON. You are already familiar with this field from the quick start.

  • Stores mappings as native JSON in the database.
  • Inherits from Django's JSONField.
  • Supports Django JSON key lookups.
  • Projected keys return native values, not DTOs.
  • JSON key projections follow Django's backend behavior. On SQLite, use KeyTextTransform when strings such as "0" or "null" must remain strings.

DTOBinaryField

Define this model in app/models.py:

>>> from django.db import models
>>> from django_dto_field.fields.binary import DTOBinaryField
>>> from app.my_dto import Customer
>>> class StoredOrder(models.Model):
...     customer_bytes = DTOBinaryField(schema=Customer)
  • Stores UTF-8 JSON bytes in the database.
  • Inherits from Django's BinaryField.
  • Is not editable by default, as with Django's BinaryField; set editable=True to include it in ModelForms.

Supported versions and databases

  • Python 3.10–3.14.
  • Django 5.2 (Python 3.10–3.14) and 6.0–6.1 (Python 3.12–3.14).
  • SQLite, PostgreSQL, MySQL, and MariaDB.

Help and contribute

Have a question, spotted a bug, or want a new DTO integration? We'd love to hear from you in the issue tracker. For bugs, include a minimal example, your Python/Django versions, and the expected and actual behavior.

Read CONTRIBUTING.md for setup, testing, and pull request guidance. Small improvements count too—clearer examples, documentation fixes, and tests are all welcome. The project uses unit, property-based, and Django integration tests with 100% statement/branch coverage, plus Ruff, WPS, and strict mypy checks.

License

Available under the MIT license.

Metadata

Release files for django-dto-field 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-dto-field 1.1.0
File Size Uploaded Provenance Actions
django_dto_field-1.1.0.tar.gz 14.9 kB Without provenance Details for django_dto_field-1.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for django-dto-field 1.1.0
File Interpreter ABI Platform Actions
django_dto_field-1.1.0-py3-none-any.whl Python 3 none any Details for django_dto_field-1.1.0-py3-none-any.whl

Total release size: 35.1 kB

Release files / django_dto_field-1.1.0.tar.gz

Download URL django_dto_field-1.1.0.tar.gz
Size 14.9 kB
Tags Source
SHA-256 checksum
How to use checksums
c0a1e81c9cab2b4561db2f135a63ac1089a6d65c2ca6fc6c43e42c6b6d9930b8
BLAKE2b-256 checksum
How to use checksums
9288dab6762ad98b8155b16b0b5b5e6da3d2c37b0a5624ed00693448ee128f2b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / django_dto_field-1.1.0-py3-none-any.whl

Download URL django_dto_field-1.1.0-py3-none-any.whl
Size 20.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c7f4df1d27120b41bef4372a30d262107667259338c27d382970607f77c7e540
BLAKE2b-256 checksum
How to use checksums
b2debe9fda2c883a0802902989b4d834135c8be5e2ecfb1c4312562167a65793
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for django-dto-field 1.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page