django-pydantic-field


Namedjango-pydantic-field JSON
Version 0.3.8 PyPI version JSON
download
home_pageNone
SummaryDjango JSONField with Pydantic models as a Schema
upload_time2024-04-23 13:46:31
maintainerNone
docs_urlNone
authorNone
requires_python>=3.7
licenseMIT License Copyright (c) 2024 Savva Surenkov and django-pydantic-field contributors. See the contributors at https://github.com/surenkov/django-pydantic-field/contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
keywords django pydantic json schema
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            [![PyPI Version](https://img.shields.io/pypi/v/django-pydantic-field)](https://pypi.org/project/django-pydantic-field/)
[![Lint and Test Package](https://github.com/surenkov/django-pydantic-field/actions/workflows/python-test.yml/badge.svg)](https://github.com/surenkov/django-pydantic-field/actions/workflows/python-test.yml)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/django-pydantic-field)](https://pypistats.org/packages/django-pydantic-field)
[![Supported Python Versions](https://img.shields.io/pypi/pyversions/django-pydantic-field)](https://pypi.org/project/django-pydantic-field/)
[![Supported Django Versions](https://img.shields.io/pypi/frameworkversions/django/django-pydantic-field)](https://pypi.org/project/django-pydantic-field/)

# Django + Pydantic = 🖤

Django JSONField with Pydantic models as a Schema.

**Now supports both Pydantic v1 and v2!** [Please join the discussion](https://github.com/surenkov/django-pydantic-field/discussions/36) if you have any thoughts or suggestions!

## 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"
```

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

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

``` python
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`](https://django-jsonform.readthedocs.io) 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:

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

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

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

## Contributing
To get `django-pydantic-field` up and running in development mode:
1. Clone this repo;
1. Create a virtual environment: `python -m venv .venv`;
1. Activate `.venv`: `. .venv/bin/activate`;
1. Install the project and its dependencies: `pip install -e .[dev,test]`;
1. Setup `pre-commit`: `pre-commit install`.

## 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;

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "django-pydantic-field",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.7",
    "maintainer_email": null,
    "keywords": "django, pydantic, json, schema",
    "author": null,
    "author_email": "Savva Surenkov <savva@surenkov.space>",
    "download_url": "https://files.pythonhosted.org/packages/36/e3/88ecc5d73f893e4f64a2bc137f8a0cd2c24a45c808131387145468848b89/django_pydantic_field-0.3.8.tar.gz",
    "platform": null,
    "description": "[![PyPI Version](https://img.shields.io/pypi/v/django-pydantic-field)](https://pypi.org/project/django-pydantic-field/)\n[![Lint and Test Package](https://github.com/surenkov/django-pydantic-field/actions/workflows/python-test.yml/badge.svg)](https://github.com/surenkov/django-pydantic-field/actions/workflows/python-test.yml)\n[![PyPI - Downloads](https://img.shields.io/pypi/dm/django-pydantic-field)](https://pypistats.org/packages/django-pydantic-field)\n[![Supported Python Versions](https://img.shields.io/pypi/pyversions/django-pydantic-field)](https://pypi.org/project/django-pydantic-field/)\n[![Supported Django Versions](https://img.shields.io/pypi/frameworkversions/django/django-pydantic-field)](https://pypi.org/project/django-pydantic-field/)\n\n# Django + Pydantic = \ud83d\udda4\n\nDjango JSONField with Pydantic models as a Schema.\n\n**Now supports both Pydantic v1 and v2!** [Please join the discussion](https://github.com/surenkov/django-pydantic-field/discussions/36) if you have any thoughts or suggestions!\n\n## Usage\n\nInstall the package with `pip install django-pydantic-field`.\n\n``` python\nimport pydantic\nfrom datetime import date\nfrom uuid import UUID\n\nfrom django.db import models\nfrom django_pydantic_field import SchemaField\n\n\nclass Foo(pydantic.BaseModel):\n    count: int\n    size: float = 1.0\n\n\nclass Bar(pydantic.BaseModel):\n    slug: str = \"foo_bar\"\n\n\nclass MyModel(models.Model):\n    # Infer schema from field annotation\n    foo_field: Foo = SchemaField()\n\n    # or explicitly pass schema to the field\n    bar_list: typing.Sequence[Bar] = SchemaField(schema=list[Bar])\n\n    # Pydantic exportable types are supported\n    raw_date_map: dict[int, date] = SchemaField()\n    raw_uids: set[UUID] = SchemaField()\n\n...\n\nmodel = MyModel(\n    foo_field={\"count\": \"5\"},\n    bar_list=[{}],\n    raw_date_map={1: \"1970-01-01\"},\n    raw_uids={\"17a25db0-27a4-11ed-904a-5ffb17f92734\"}\n)\nmodel.save()\n\nassert model.foo_field == Foo(count=5, size=1.0)\nassert model.bar_list == [Bar(slug=\"foo_bar\")]\nassert model.raw_date_map == {1: date(1970, 1, 1)}\nassert model.raw_uids == {UUID(\"17a25db0-27a4-11ed-904a-5ffb17f92734\")}\n```\n\nPractically, schema could be of any type supported by Pydantic.\nIn addition, an external `config` class can be passed for such schemes.\n\n### Forward referencing annotations\n\nIt is also possible to use `SchemaField` with forward references and string literals, e.g the code below is also valid:\n\n``` python\n\nclass MyModel(models.Model):\n    foo_field: \"Foo\" = SchemaField()\n    bar_list: typing.Sequence[\"Bar\"] = SchemaField(schema=typing.ForwardRef(\"list[Bar]\"))\n\n\nclass Foo(pydantic.BaseModel):\n    count: int\n    size: float = 1.0\n\n\nclass Bar(pydantic.BaseModel):\n    slug: str = \"foo_bar\"\n```\n\n**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.\n\nTo 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.\n\nHere's the list of currently implemented checks:\n- `pydantic.E001`: The passed schema could not be resolved. Most likely it does not exist in the scope of the defined field.\n- `pydantic.E002`: `default=` value could not be serialized to the schema.\n- `pydantic.W003`: The default value could not be reconstructed to the schema due to `include`/`exclude` configuration.\n\n\n### `typing.Annotated` support\nAs 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.\n\n**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.\nFor 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.\n\n## Django Forms support\n\nIt is possible to create Django forms, which would validate against the given schema:\n\n``` python\nfrom django import forms\nfrom django_pydantic_field.forms import SchemaField\n\n\nclass Foo(pydantic.BaseModel):\n    slug: str = \"foo_bar\"\n\n\nclass FooForm(forms.Form):\n    field = SchemaField(Foo)  # `typing.ForwardRef(\"Foo\")` is fine too, but only in Django 4+\n\n\nform = FooForm(data={\"field\": '{\"slug\": \"asdf\"}'})\nassert form.is_valid()\nassert form.cleaned_data[\"field\"] == Foo(slug=\"asdf\")\n```\n\n`django_pydantic_field` also supports auto-generated fields for `ModelForm` and `modelform_factory`:\n\n``` python\nclass MyModelForm(forms.ModelForm):\n    class Meta:\n        model = MyModel\n        fields = [\"foo_field\"]\n\nform = MyModelForm(data={\"foo_field\": '{\"count\": 5}'})\nassert form.is_valid()\nassert form.cleaned_data[\"foo_field\"] == Foo(count=5)\n\n...\n\n# ModelForm factory support\nAnotherModelForm = modelform_factory(MyModel, fields=[\"foo_field\"])\nform = AnotherModelForm(data={\"foo_field\": '{\"count\": 5}'})\n\nassert form.is_valid()\nassert form.cleaned_data[\"foo_field\"] == Foo(count=5)\n```\n\nNote, that forward references would be resolved until field is being bound to the form instance.\n\n### `django-jsonform` widgets\n[`django-jsonform`](https://django-jsonform.readthedocs.io) offers a dynamic form construction based on the specified JSONSchema.\n`django_pydantic_field.forms.SchemaField` plays nicely with its widgets, but only for Pydantic v2:\n\n``` python\nfrom django_pydantic_field.forms import SchemaField\nfrom django_jsonform.widgets import JSONFormWidget\n\nclass FooForm(forms.Form):\n    field = SchemaField(Foo, widget=JSONFormWidget)\n```\n\nIt is also possible to override the default form widget for Django Admin site, without writing custom admin forms:\n\n``` python\nfrom django.contrib import admin\nfrom django_jsonform.widgets import JSONFormWidget\n\n# NOTE: Importing direct field class instead of `SchemaField` wrapper.\nfrom django_pydantic_field.v2.fields import PydanticSchemaField\n\n@admin.site.register(MyModel)\nclass MyModelAdmin(admin.ModelAdmin):\n    formfield_overrides = {\n        PydanticSchemaField: {\"widget\": JSONFormWidget},\n    }\n```\n\n## Django REST Framework support\n\n``` python\nfrom rest_framework import generics, serializers\nfrom django_pydantic_field.rest_framework import SchemaField, AutoSchema\n\n\nclass MyModelSerializer(serializers.ModelSerializer):\n    foo_field = SchemaField(schema=Foo)\n\n    class Meta:\n        model = MyModel\n        fields = '__all__'\n\n\nclass SampleView(generics.RetrieveAPIView):\n    serializer_class = MyModelSerializer\n\n    # optional support of OpenAPI schema generation for Pydantic fields\n    schema = AutoSchema()\n```\n\nGlobal approach with typed `parser` and `renderer` classes\n``` python\nfrom rest_framework import views\nfrom rest_framework.decorators import api_view, parser_classes, renderer_classes\nfrom django_pydantic_field.rest_framework import SchemaRenderer, SchemaParser, AutoSchema\n\n\n@api_view([\"POST\"])\n@parser_classes([SchemaParser[Foo]]):\n@renderer_classes([SchemaRenderer[list[Foo]]])\ndef foo_view(request):\n    assert isinstance(request.data, Foo)\n\n    count = request.data.count + 1\n    return Response([Foo(count=count)])\n\n\nclass FooClassBasedView(views.APIView):\n    parser_classes = [SchemaParser[Foo]]\n    renderer_classes = [SchemaRenderer[list[Foo]]]\n\n    # optional support of OpenAPI schema generation for Pydantic parsers/renderers\n    schema = AutoSchema()\n\n    def get(self, request, *args, **kwargs):\n        assert isinstance(request.data, Foo)\n        return Response([request.data])\n\n    def put(self, request, *args, **kwargs):\n        assert isinstance(request.data, Foo)\n\n        count = request.data.count + 1\n        return Response([request.data])\n```\n\n## Contributing\nTo get `django-pydantic-field` up and running in development mode:\n1. Clone this repo;\n1. Create a virtual environment: `python -m venv .venv`;\n1. Activate `.venv`: `. .venv/bin/activate`;\n1. Install the project and its dependencies: `pip install -e .[dev,test]`;\n1. Setup `pre-commit`: `pre-commit install`.\n\n## Acknowledgement\n\n* [Churkin Oleg](https://gist.github.com/Bahus/98a9848b1f8e2dcd986bf9f05dbf9c65) for his Gist as a source of inspiration;\n* Boutique Air Flight Operations platform as a test ground;\n",
    "bugtrack_url": null,
    "license": "MIT License  Copyright (c) 2024 Savva Surenkov and django-pydantic-field contributors. See the contributors at https://github.com/surenkov/django-pydantic-field/contributors  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ",
    "summary": "Django JSONField with Pydantic models as a Schema",
    "version": "0.3.8",
    "project_urls": {
        "Changelog": "https://github.com/surenkov/django-pydantic-field/releases",
        "Documentation": "https://github.com/surenkov/django-pydantic-field",
        "Homepage": "https://github.com/surenkov/django-pydantic-field",
        "Source": "https://github.com/surenkov/django-pydantic-field"
    },
    "split_keywords": [
        "django",
        " pydantic",
        " json",
        " schema"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "a36b38795979ddd691c92c15af99e3c4a3f5728b9b271be79c060a5a7385e90a",
                "md5": "40a1d058a93d88d426c8be8c74b9cbe6",
                "sha256": "67a0356e54f86184e6f7272f6c633566f489276d7c37d5fc4893ce3ce3a8f5f5"
            },
            "downloads": -1,
            "filename": "django_pydantic_field-0.3.8-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "40a1d058a93d88d426c8be8c74b9cbe6",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.7",
            "size": 42100,
            "upload_time": "2024-04-23T13:46:29",
            "upload_time_iso_8601": "2024-04-23T13:46:29.330802Z",
            "url": "https://files.pythonhosted.org/packages/a3/6b/38795979ddd691c92c15af99e3c4a3f5728b9b271be79c060a5a7385e90a/django_pydantic_field-0.3.8-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "36e388ecc5d73f893e4f64a2bc137f8a0cd2c24a45c808131387145468848b89",
                "md5": "4e0d4adddcf37fe381773485e8086f56",
                "sha256": "1a0ffe0c46fad90d8d84a0ee5ab8877d5082ae3728a578cab6563ce647b2a348"
            },
            "downloads": -1,
            "filename": "django_pydantic_field-0.3.8.tar.gz",
            "has_sig": false,
            "md5_digest": "4e0d4adddcf37fe381773485e8086f56",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.7",
            "size": 38199,
            "upload_time": "2024-04-23T13:46:31",
            "upload_time_iso_8601": "2024-04-23T13:46:31.277760Z",
            "url": "https://files.pythonhosted.org/packages/36/e3/88ecc5d73f893e4f64a2bc137f8a0cd2c24a45c808131387145468848b89/django_pydantic_field-0.3.8.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-04-23 13:46:31",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "surenkov",
    "github_project": "django-pydantic-field",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "django-pydantic-field"
}
        
Elapsed time: 0.44831s