Metadata-Version: 2.1
Name: django-pydantic-field
Version: 0.2.0
Summary: Django JSONField with Pydantic models as a Schema
Home-page: https://github.com/surenkov/django-pydantic-field
Author: Savva Surenkov
Author-email: savva@surenkov.space
Keywords: django,pydantic,json,schema
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Framework :: Django
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Requires-Python: >=3.7
Description-Content-Type: text/markdown
Provides-Extra: rest
Provides-Extra: dev
License-File: LICENSE

[![PyPI Version](https://img.shields.io/pypi/v/django-pydantic-field)](https://pypi.org/project/django-pydantic-field/)
[![Supported Python Versions](https://img.shields.io/pypi/pyversions/django_pydantic_field)](https://github.com/surenkov/django-pydantic-field)

# Django + Pydantic = 🖤

Django JSONField with Pydantic models as a Schema

## Usage

Install the package with `pip install django-pydantic-field`.

``` python
import pydantic
from datetime import date
from uuid import UUID

from django.db import models
from django_pydantic_field import SchemaField


class Foo(pydantic.BaseModel):
    count: int
    size: float = 1.0


class Bar(pydantic.BaseModel):
    slug: str = "foo_bar"


class MyModel(models.Model):
    # Infer schema from field annotation
    foo_field: Foo = SchemaField()

    # or explicitly pass schema to the field
    bar_list: typing.Sequence[Bar] = SchemaField(schema=list[Bar])

    # Pydantic exportable types are supported
    raw_date_map: dict[int, date] = SchemaField()
    raw_uids: set[UUID] = SchemaField()

...
    
model = MyModel(
    foo_field={"count": "5"},
    bar_list=[{}],
    raw_date_map={1: "1970-01-01"},
    raw_uids={"17a25db0-27a4-11ed-904a-5ffb17f92734"}
)
model.save()

assert model.foo_field == Foo(count=5, size=1.0)
assert model.bar_list == [Bar(slug="foo_bar")]
assert model.raw_date_map = {1: date(1970, 1, 1)}
assert model.raw_uids = {UUID("17a25db0-27a4-11ed-904a-5ffb17f92734")}
```

Practically, schema could be of any type supported by Pydantic.
In addition, an external `config` class can be passed for such schemes.

### Forward referencing annotations

It is also possible to use `SchemaField` with forward references and string literals, e.g the code below is also valid:

``` python

class MyModel(models.Model):
    foo_field: "Foo" = SchemaField()
    bar_list: typing.Sequence["Bar"] = SchemaField(schema=typing.ForwardRef("list[Bar]"))


class Foo(pydantic.BaseModel):
    count: int
    size: float = 1.0


class Bar(pydantic.BaseModel):
    slug: str = "foo_bar"
```

In this case, exact type resolution will be postponed until initial access to the field.
Usually this happens on the first instantiation of the model.

## Django Forms support

It is possible to create Django forms, which would validate against the given schema:

``` python
from django import forms
from django_pydantic_field.forms import SchemaField


class Foo(pydantic.BaseModel):
    slug: str = "foo_bar"


class FooForm(forms.Form):
    field = SchemaField(Foo)  # `typing.ForwardRef("Foo")` is fine too, but only in Django 4+


form = FooMForm(data={"field": '{"slug": "asdf"}'})
assert form.is_valid()
assert form.cleaned_data["field"] == Foo(slug="asdf")
```

`django_pydantic_field` also supports auto-generated fields for `ModelForm` and `modelform_factory`:

``` python
class FooModelForm(forms.ModelForm):
    class Meta:
        model = Foo
        fields = ["field"]

form = FooModelForm(data={"field": '{"slug": "asdf"}'})
assert form.is_valid()
assert form.cleaned_data["field"] == Foo(slug="asdf")

...

# ModelForm factory support
AnotherFooModelForm = modelform_factory(Foo, fields=["field"])
form = AnotherFooModelForm(data={"field": '{"slug": "bar_baz"}'})

assert form.is_valid()
assert form.cleaned_data["field"] == Foo(slug="bar_baz")
```

Note, that forward references would be resolved until field is being bound to the form instance.

## Django REST Framework support

``` python
from rest_framework import generics, serializers
from django_pydantic_field.rest_framework import SchemaField, AutoSchema


class MyModelSerializer(serializers.ModelSerializer):
    foo_field = SchemaField(schema=Foo)

    class Meta:
        model = MyModel
        fields = '__all__'


class SampleView(generics.RetrieveAPIView):
    serializer_class = MyModelSerializer

    # optional support of OpenAPI schema generation for Pydantic fields
    schema = AutoSchema()
```

Global approach with typed `parser` and `renderer` classes
``` python
from rest_framework import views
from rest_framework.decorators import api_view, parser_classes, renderer_classes
from django_pydantic_field.rest_framework import SchemaRenderer, SchemaParser, AutoSchema


@api_view(["POST"])
@parser_classes([SchemaParser[Foo]]):
@renderer_classes([SchemaRenderer[list[Foo]]])
def foo_view(request):
    assert isinstance(request.data, Foo)

    count = request.data.count + 1
    return Response([Foo(count=count)])


class FooClassBasedView(views.APIView):
    parser_classes = [SchemaParser[Foo]]
    renderer_classes = [SchemaRenderer[list[Foo]]]

    # optional support of OpenAPI schema generation for Pydantic parsers/renderers
    schema = AutoSchema()

    def get(self, request, *args, **kwargs):
        assert isinstance(request.data, Foo)
        return Response([request.data])

    def put(self, request, *args, **kwargs):
        assert isinstance(request.data, Foo)

        count = request.data.count + 1
        return Response([request.data])
```

## Acknowledgement

* [Churkin Oleg](https://gist.github.com/Bahus/98a9848b1f8e2dcd986bf9f05dbf9c65) for his Gist as a source of inspiration;
* Boutique Air Flight Operations platform as a test ground;

