Skip to main content

Django JSONField with Pydantic models as a Schema

Project description

PyPI Version Lint and Test Package PyPI - Downloads Supported Python Versions Supported Django Versions

Django + Pydantic = 🖤

Django JSONField with Pydantic models as a Schema.

Now supports both Pydantic v1 and v2! Please join the discussion if you have any thoughts or suggestions!

Usage

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

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:

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"

Pydantic v2 specific: this behaviour is achieved by the fact that the exact type resolution will be postponed until the initial access to the field. Usually this happens on the first instantiation of the model.

To reduce the number of runtime errors related to the postponed resolution, the field itself performs a few checks against the passed schema during ./manage.py check command invocation, and consequently, in runserver and makemigrations commands.

Here's the list of currently implemented checks:

  • pydantic.E001: The passed schema could not be resolved. Most likely it does not exist in the scope of the defined field.
  • pydantic.E002: default= value could not be serialized to the schema.
  • pydantic.W003: The default value could not be reconstructed to the schema due to include/exclude configuration.

typing.Annotated support

As of v0.3.5, SchemaField also supports typing.Annotated[...] expressions, both through schema= attribute or field annotation syntax; though I find the schema=typing.Annotated[...] variant highly discouraged.

The current limitation is not in the field itself, but in possible Annotated metadata -- practically it can contain anything, and Django migrations serializers could refuse to write it to migrations. For most relevant types in context of Pydantic, I wrote the specific serializers (particularly for pydantic.FieldInfo, pydantic.Representation and raw dataclasses), thus it should cover the majority of Annotated use cases.

Django Forms support

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

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 = FooForm(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:

class MyModelForm(forms.ModelForm):
    class Meta:
        model = MyModel
        fields = ["foo_field"]

form = MyModelForm(data={"foo_field": '{"count": 5}'})
assert form.is_valid()
assert form.cleaned_data["foo_field"] == Foo(count=5)

...

# ModelForm factory support
AnotherModelForm = modelform_factory(MyModel, fields=["foo_field"])
form = AnotherModelForm(data={"foo_field": '{"count": 5}'})

assert form.is_valid()
assert form.cleaned_data["foo_field"] == Foo(count=5)

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

django-jsonform widgets

django-jsonform offers a dynamic form construction based on the specified JSONSchema. django_pydantic_field.forms.SchemaField plays nicely with its widgets, but only for Pydantic v2:

from django_pydantic_field.forms import SchemaField
from django_jsonform.widgets import JSONFormWidget

class FooForm(forms.Form):
    field = SchemaField(Foo, widget=JSONFormWidget)

It is also possible to override the default form widget for Django Admin site, without writing custom admin forms:

from django.contrib import admin
from django_jsonform.widgets import JSONFormWidget

# NOTE: Importing direct field class instead of `SchemaField` wrapper.
from django_pydantic_field.v2.fields import PydanticSchemaField

@admin.site.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
    formfield_overrides = {
        PydanticSchemaField: {"widget": JSONFormWidget},
    }

Django REST Framework support

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

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])

Contributing

To get django-pydantic-field up and running in development mode:

  1. Clone this repo;
  2. Create a virtual environment: python -m venv .venv;
  3. Activate .venv: . .venv/bin/activate;
  4. Install the project and its dependencies: pip install -e .[dev,test];
  5. Setup pre-commit: pre-commit install.

Acknowledgement

  • Churkin Oleg for his Gist as a source of inspiration;
  • Boutique Air Flight Operations platform as a test ground;

Project details


Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_pydantic_field-0.3.12.tar.gz (38.3 kB view details)

Uploaded Source

Built Distribution

django_pydantic_field-0.3.12-py3-none-any.whl (42.4 kB view details)

Uploaded Python 3

File details

Details for the file django_pydantic_field-0.3.12.tar.gz.

File metadata

  • Download URL: django_pydantic_field-0.3.12.tar.gz
  • Upload date:
  • Size: 38.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.0.1 CPython/3.12.8

File hashes

Hashes for django_pydantic_field-0.3.12.tar.gz
Algorithm Hash digest
SHA256 510af19d2813c32a2529234a70eb6b6bd8e75fcc4f53baee520f797cba3420d0
MD5 356eaa2234cd434dc0e14d80643d9b1b
BLAKE2b-256 46d48be5efc5dc2784a5c662744221a8e8ffc1df6d5866237b5e30c8f92e7d0c

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_pydantic_field-0.3.12.tar.gz:

Publisher: python-publish.yml on surenkov/django-pydantic-field

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_pydantic_field-0.3.12-py3-none-any.whl.

File metadata

File hashes

Hashes for django_pydantic_field-0.3.12-py3-none-any.whl
Algorithm Hash digest
SHA256 57aeb055583e7ae82732cfaa36afd01166c7000a67502a9922dca1ce9b3e76a5
MD5 ebb928adf4fef7a137219d0692da4eaa
BLAKE2b-256 8292bd13828e0d50dbbec496618471dd07727a645b8464f2431d2cbf2a126952

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_pydantic_field-0.3.12-py3-none-any.whl:

Publisher: python-publish.yml on surenkov/django-pydantic-field

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page