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!
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.0.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.0.0.tar.gz | 13.6 kB | Without provenance | Details for django_dto_field-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| django_dto_field-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for django_dto_field-1.0.0-py3-none-any.whl |
Total release size: 32.6 kB
Release files / django_dto_field-1.0.0.tar.gz
| Download URL | django_dto_field-1.0.0.tar.gz |
|---|---|
| Size | 13.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
56f77dda66c2df2a3379579179730f49ef8b5a56943a86733d9a95df331e3334
|
|
BLAKE2b-256 checksum How to use checksums |
20b31dea97296dc20d6b96c1ad30e317b9830aa659e66b1fef742e3c484b909c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","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.0.0-py3-none-any.whl
| Download URL | django_dto_field-1.0.0-py3-none-any.whl |
|---|---|
| Size | 19.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
812ea12012a1be69053666247ff908d3f0d9794aba1ce7318d5a160cfbf982f9
|
|
BLAKE2b-256 checksum How to use checksums |
31a20a5f9528d215824a7ad31510cfa7861ee04ebdb7fe8b6054498e19d2aee4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","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.0.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.