Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
django-pydantic-field
Advanced tools
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!
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.
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
supportAs 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.
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
widgetsdjango-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},
}
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])
To get django-pydantic-field
up and running in development mode:
python -m venv .venv
;.venv
: . .venv/bin/activate
;pip install -e .[dev,test]
;pre-commit
: pre-commit install
.FAQs
Django JSONField with Pydantic models as a Schema
We found that django-pydantic-field demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.