django-dto-field
Your DTO's, at home in Django models.
Save a your DTO object (dict, dataclass, etc) and get it back—including nested objects. Let the field
handle conversion and schema validation while you work with familiar objects and Django framework.
Why use it?
Django's JSONField stores structured data, but does not reconstruct your typed
objects or validate their annotated members. Applications often repeat that
conversion in model methods, services, and forms.
django-dto-field puts that work at the field boundary. Write a DTO, read a DTO,
and keep schema validation consistent across model saves, queryset updates, and
bulk writes. Choose native JSON, text, or binary storage to suit your database.
- DTO's work out of the box, with nested objects and strict member validation.
- Dictionaries DTO's work without a schema when you need flexible structured data.
- JSON fields retain Django's key lookups and projections.
- ModelForms display JSON, and Django JSON fixtures round-trip DTO values.
- Explicit adapters provide an extension point for other DTO libraries.
DTO support
- Python
dataclasses— nested DTOs, annotated-member validation, and reconstruction on reads. - Python
dict— flexible JSON-compatible data without a custom schema. - Pydantic — planned model adapter.
- Marshmallow — planned schema adapter.
- attrs — planned object adapter.
- Adaptix — planned conversion adapter.
The adapter architecture is already in place for these integrations. Want to help bring your favorite library to Django fields? See Extend DTO support and our contributing guide.
Install
Requires Python 3.10+ and Django 4.2+.
pip install django-dto-field
No additional entry in INSTALLED_APPS is needed. Add the fields to your models,
then create and apply migrations as usual.
Quick start
dataclass
Let's give an order a typed customer and address. Put these classes in your app's
models.py (or another importable module) so Django can use them in migrations:
from dataclasses import dataclass
from django.db import models
from django_dto_field.fields.json import DTOJSONField
@dataclass
class Address:
city: str
@dataclass
class Customer:
name: str
address: Address
class Order(models.Model):
customer = DTOJSONField(schema=Customer)
After creating and applying migrations, try this in the Django shell:
from myapp.models import Address, Customer, Order
order = Order.objects.create(customer=Customer("Alice", Address("London")))
order.refresh_from_db()
assert order.customer == Customer("Alice", Address("London"))
assert isinstance(order.customer.address, Address)
Order.objects.filter(customer__address__city="London")
Order.objects.values_list("customer__address", flat=True) # Returns dictionaries.
You can pass a dictionary too. full_clean() turns it into your dataclass on the
model instance:
order = Order(customer={"name": "Bob", "address": {"city": "Paris"}})
order.full_clean()
assert isinstance(order.customer, Customer)
order.save()
Invalid annotated members raise Django ValidationError during validation or
literal writes—even when Python permits constructing the dataclass:
from django.core.exceptions import ValidationError
try:
Order.objects.create(customer=Customer("Alice", Address(city=123)))
except ValidationError as error:
print(error)
dict
For data that doesn't need a typed schema, leave out schema:
from django.db import models
from django_dto_field.fields.json import DTOJSONField
class Document(models.Model):
metadata = DTOJSONField(default=dict, blank=True)
After migrating, use the field in the Django shell or application code:
from myapp.models import Document
document = Document.objects.create(metadata={"author": "Ada", "tags": ["math"]})
document.refresh_from_db()
assert document.metadata == {"author": "Ada", "tags": ["math"]}
Document.objects.filter(metadata__author="Ada")
Document.objects.values_list("metadata__tags", flat=True) # Returns lists.
Use callable defaults such as dict so each instance gets its own value.
Choose your storage field
The schema and adapter options work with all three fields:
| Import | Django base | Stored value |
|---|---|---|
django_dto_field.fields.json.DTOJSONField |
JSONField |
Native JSON, encoded by Django |
django_dto_field.fields.char.DTOCharField |
CharField |
UTF-8 JSON text |
django_dto_field.fields.binary.DTOBinaryField |
BinaryField |
UTF-8 JSON bytes |
from django_dto_field.fields.binary import DTOBinaryField
from django_dto_field.fields.char import DTOCharField
# Inside a model definition:
summary = DTOCharField(max_length=2000, default=dict, blank=True)
archive = DTOBinaryField(schema=Customer, null=True, blank=True)
DTOCharField.max_length measures the serialized text. DTOBinaryField retains
Django's editable=False default. Package initializers do not re-export classes;
use the module paths above.
A few things worth knowing
- Validation timing: attribute assignment does not validate. Conversion and
literal writes validate the schema, including
update()and bulk writes. Callfull_clean()for Django field validators;save()does not run them all. - In-memory values: saving dictionary input prepares it for storage without
replacing the attribute. Use
full_clean()orrefresh_from_db()to obtain a DTO. - Field validators: receive the storage value—text, bytes, or a JSON-compatible
mapping.
nullandblankretain Django semantics. - JSON lookups: partial/scalar operands bypass whole-DTO validation. Key projections return native JSON values, not partial DTO's.
- Nulls:
Nonestores SQL NULL when permitted.DTOJSONFieldalso supports explicit JSON null through Django'sValue(None, output_field=...). - SQL expressions: arbitrary database expressions are not schema-validated before execution; complete values are validated when read back.
- Schema changes: changing a schema does not rewrite existing data. Plan a data migration when stored values no longer satisfy the new schema.
Extend DTO support
If you'd like to build your custom integration supply an importable adapter class:
from django_dto_field.fields.json import DTOJSONField
from myapp.dto import MyAdapter, MySchema
payload = DTOJSONField(schema=MySchema, adapter=MyAdapter)
Implement the DTOAdapter protocol from django_dto_field.adapters.base:
__init__(schema): configure and validate the schema.is_instance(instance): recognize a DTO usingTypeGuard.to_data(instance): produce a JSON-compatible dictionary.from_data(payload): validate a complete dictionary and reconstruct a DTO.
The schema and result classes may differ. Adapters are instantiated per converter
and preserved in migrations, with no global registry. Translate integration
errors into library exceptions from django_dto_field.exceptions.base.
At the Django boundary, these become DTOFieldError from
django_dto_field.exceptions.django, which also inherits Django ValidationError.
See the test adapters for small working examples.
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 0.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-0.1.0.tar.gz | 11.2 kB | Without provenance | Details for django_dto_field-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| django_dto_field-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for django_dto_field-0.1.0-py3-none-any.whl |
Total release size: 29.8 kB
Release files / django_dto_field-0.1.0.tar.gz
| Download URL | django_dto_field-0.1.0.tar.gz |
|---|---|
| Size | 11.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2d2f6d49eafc9c37f8d0284b5da67d960e6de931a764867e370508722a8f4b9e
|
|
BLAKE2b-256 checksum How to use checksums |
c3f36ce5bf39c50785abc56233b608c9afd8c20e858e5bc0e8387ce2e4003e5a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","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-0.1.0-py3-none-any.whl
| Download URL | django_dto_field-0.1.0-py3-none-any.whl |
|---|---|
| Size | 18.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
082627501812bcbad40590cab4784ea0b1b3a5d4472c3fab19168aa13251c78b
|
|
BLAKE2b-256 checksum How to use checksums |
6bc412f0697006dc19916794102c13f3cba6f98b7b6fb03ddd938418e2681708
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","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 0.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.