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
- Quick start
- No schema is set
- Supported DTO types
- Django Fields
- Supported versions and databases
- Help and contribute
- 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. RaiseSchemaErrorif 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. RaiseSerializationErrorif serialization fails.from_data(payload): Validate dictionary data and reconstruct a DTO instance. RaiseDTOValidationErrorif 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
KeyTextTransformwhen 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; seteditable=Trueto 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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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.