Metadata-Version: 2.4
Name: django-dto-field
Version: 0.1.0
Summary: Storing DTO data in easy-to-use and production-ready Django Model Field with fast [de]serialization.
Author: skv0zsneg
Author-email: skv0zsneg <<klimentlamonov@yandex.ru>>
License-Expression: MIT
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Dist: django>=4.2.0
Requires-Dist: msgspec>=0.20.0,<0.22.0
Requires-Python: >=3.10, <4.0
Description-Content-Type: text/markdown

# django-dto-field

[![PyPI](https://img.shields.io/pypi/v/django-dto-field.svg)](https://pypi.org/project/django-dto-field/)
[![Tests](https://github.com/skv0zsneg/django-dto-field/actions/workflows/test.yml/badge.svg)](https://github.com/skv0zsneg/django-dto-field/actions/workflows/test.yml)
[![typing & lint](https://github.com/skv0zsneg/django-dto-field/actions/workflows/typing_and_lint.yml/badge.svg?event=push)](https://github.com/skv0zsneg/django-dto-field/actions/workflows/typing_and_lint.yml)

[![wemake-python-styleguide](https://img.shields.io/badge/style-wemake-000000.svg)](https://github.com/wemake-services/wemake-python-styleguide)

**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

- [x] Python [`dataclasses`](https://docs.python.org/3/library/dataclasses.html) —
  nested DTOs, annotated-member validation, and reconstruction on reads.
- [x] Python `dict` — flexible JSON-compatible data without a custom schema.
- [ ] [Pydantic](https://docs.pydantic.dev/) — planned model adapter.
- [ ] [Marshmallow](https://marshmallow.readthedocs.io/) — planned schema adapter.
- [ ] [attrs](https://www.attrs.org/) — planned object adapter.
- [ ] [Adaptix](https://adaptix.readthedocs.io/) — 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](#extend-dto-support)
and our [contributing guide](CONTRIBUTING.md).

## Install

Requires **Python 3.10+** and **Django 4.2+**.

```bash
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:

```python
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:

```python
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:

```python
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:

```python
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`:

```python
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:

```python
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 |

```python
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.
  Call `full_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()` or `refresh_from_db()` to obtain a DTO.
- **Field validators:** receive the storage value—text, bytes, or a JSON-compatible
  mapping. `null` and `blank` retain Django semantics.
- **JSON lookups:** partial/scalar operands bypass whole-DTO validation. Key
  projections return native JSON values, not partial DTO's.
- **Nulls:** `None` stores SQL NULL when permitted. `DTOJSONField` also supports
  explicit JSON null through Django's `Value(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:

```python
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 using `TypeGuard`.
- `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](tests/e2e/dict_field/adapters.py) 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](https://github.com/skv0zsneg/django-dto-field/issues).
For bugs, include a minimal example, your Python/Django versions, and the expected
and actual behavior.

Read [CONTRIBUTING.md](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](LICENSE).
