Package: django-pydantic-field
Django + Pydantic = 🖤Django JSONField with Pydantic models as a Schema.Now supports both Pydantic v1 and v2!Please join the discussionif you have any thoughts or suggestions!UsageInstall the package withpip install django-pydantic-field.importpydanticfromdatetimeimportdatefromuuidimportUUIDfromdjango.dbimportmodelsfromdjango_pydantic_fieldimportSchemaFieldclassFoo(pydantic.BaseModel):count:intsize:float=1.0classBar(pydantic.BaseModel):slug:str="foo_bar"classMyModel(models.Model):# Infer schema from field annotationfoo_field:Foo=SchemaField()# or explicitly pass schema to the fieldbar_list:typing.Sequence[Bar]=SchemaField(schema=list[Bar])# Pydantic exportable types are supportedraw_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()assertmodel.foo_field==Foo(count=5,size=1.0)assertmodel.bar_list==[Bar(slug="foo_bar")]assertmodel.raw_date_map=={1:date(1970,1,1)}assertmodel.raw_uids=={UUID("17a25db0-27a4-11ed-904a-5ffb17f92734")}Practically, schema could be of any type supported by Pydantic.
In addition, an externalconfigclass can be passed for such schemes.Forward referencing annotationsIt is also possible to useSchemaFieldwith forward references and string literals, e.g the code below is also valid:classMyModel(models.Model):foo_field:"Foo"=SchemaField()bar_list:typing.Sequence["Bar"]=SchemaField(schema=typing.ForwardRef("list[Bar]"))classFoo(pydantic.BaseModel):count:intsize:float=1.0classBar(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 supportIt is possible to create Django forms, which would validate against the given schema:fromdjangoimportformsfromdjango_pydantic_field.formsimportSchemaFieldclassFoo(pydantic.BaseModel):slug:str="foo_bar"classFooForm(forms.Form):field=SchemaField(Foo)# `typing.ForwardRef("Foo")` is fine too, but only in Django 4+form=FooForm(data={"field":'{"slug": "asdf"}'})assertform.is_valid()assertform.cleaned_data["field"]==Foo(slug="asdf")django_pydantic_fieldalso supports auto-generated fields forModelFormandmodelform_factory:classFooModelForm(forms.ModelForm):classMeta:model=Foofields=["field"]form=FooModelForm(data={"field":'{"slug": "asdf"}'})assertform.is_valid()assertform.cleaned_data["field"]==Foo(slug="asdf")...# ModelForm factory supportAnotherFooModelForm=modelform_factory(Foo,fields=["field"])form=AnotherFooModelForm(data={"field":'{"slug": "bar_baz"}'})assertform.is_valid()assertform.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 supportfromrest_frameworkimportgenerics,serializersfromdjango_pydantic_field.rest_frameworkimportSchemaField,AutoSchemaclassMyModelSerializer(serializers.ModelSerializer):foo_field=SchemaField(schema=Foo)classMeta:model=MyModelfields='__all__'classSampleView(generics.RetrieveAPIView):serializer_class=MyModelSerializer# optional support of OpenAPI schema generation for Pydantic fieldsschema=AutoSchema()Global approach with typedparserandrendererclassesfromrest_frameworkimportviewsfromrest_framework.decoratorsimportapi_view,parser_classes,renderer_classesfromdjango_pydantic_field.rest_frameworkimportSchemaRenderer,SchemaParser,AutoSchema@api_view(["POST"])@parser_classes([SchemaParser[Foo]]):@renderer_classes([SchemaRenderer[list[Foo]]])deffoo_view(request):assertisinstance(request.data,Foo)count=request.data.count+1returnResponse([Foo(count=count)])classFooClassBasedView(views.APIView):parser_classes=[SchemaParser[Foo]]renderer_classes=[SchemaRenderer[list[Foo]]]# optional support of OpenAPI schema generation for Pydantic parsers/renderersschema=AutoSchema()defget(self,request,*args,**kwargs):assertisinstance(request.data,Foo)returnResponse([request.data])defput(self,request,*args,**kwargs):assertisinstance(request.data,Foo)count=request.data.count+1returnResponse([request.data])AcknowledgementChurkin Olegfor his Gist as a source of inspiration;Boutique Air Flight Operations platform as a test ground;