Django JSONField with msgspec structs as a Schema. Fork of django-pydantic-field.
Project description
Django + msgspec = 🖤
Django JSONField with msgspec structs as a Schema.
Note: This library is a fork of django-pydantic-field that replaces Pydantic with msgspec for faster serialization and validation.
Why msgspec?
msgspec is a fast serialization library that offers:
- High performance: 10-75x faster than other serialization libraries
- Low memory usage: More memory-efficient than alternatives
- Type validation: Built-in support for Python type hints
- JSON Schema generation: Automatic schema generation for API documentation
Installation
Install the package with pip:
pip install django-msgspec-field
Usage
import msgspec
from datetime import date
from uuid import UUID
from django.db import models
from django_msgspec_field import SchemaField
class Foo(msgspec.Struct):
count: int
size: float = 1.0
class Bar(msgspec.Struct):
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: list[Bar] = SchemaField(schema=list[Bar])
# msgspec supports many types natively
raw_date_map: dict[int, date] = SchemaField()
raw_uids: set[UUID] = SchemaField()
# Usage
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")}
The schema can be any type supported by msgspec, including:
msgspec.Structclassesdataclassestyping.TypedDict- Standard Python types (
list,dict,set, etc.) - Unions, Optionals, and other generic types
Forward referencing annotations
It is also possible to use SchemaField with forward references and string literals:
class MyModel(models.Model):
foo_field: "Foo" = SchemaField()
bar_list: list["Bar"] = SchemaField(schema=typing.ForwardRef("list[Bar]"))
class Foo(msgspec.Struct):
count: int
size: float = 1.0
class Bar(msgspec.Struct):
slug: str = "foo_bar"
The exact type resolution will be postponed until the initial access to the field, which usually happens on the first instantiation of the model.
The field performs checks against the passed schema during ./manage.py check command invocation:
msgspec.E001: The passed schema could not be resolved.msgspec.E002:default=value could not be serialized to the schema.msgspec.W003: The default value could not be reconstructed due toinclude/excludeconfiguration.
typing.Annotated support
SchemaField supports typing.Annotated[...] expressions for adding constraints:
import typing_extensions as te
import msgspec
class MyModel(models.Model):
annotated_field: te.Annotated[int, msgspec.Meta(gt=0, title="Positive Integer")] = SchemaField()
Django Forms support
Create Django forms that validate against msgspec schemas:
from django import forms
from django_msgspec_field.forms import SchemaField
class Foo(msgspec.Struct):
slug: str = "foo_bar"
class FooForm(forms.Form):
field = SchemaField(Foo)
form = FooForm(data={"field": '{"slug": "asdf"}'})
assert form.is_valid()
assert form.cleaned_data["field"] == Foo(slug="asdf")
django_msgspec_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)
django-jsonform widgets
django-jsonform offers dynamic form construction based on JSONSchema. django_msgspec_field.forms.SchemaField works with its widgets:
from django_msgspec_field.forms import SchemaField
from django_jsonform.widgets import JSONFormWidget
class FooForm(forms.Form):
field = SchemaField(Foo, widget=JSONFormWidget)
Override the default form widget for Django Admin:
from django.contrib import admin
from django_jsonform.widgets import JSONFormWidget
from django_msgspec_field.fields import MsgspecSchemaField
@admin.site.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
formfield_overrides = {
MsgspecSchemaField: {"widget": JSONFormWidget},
}
Django REST Framework support
from rest_framework import generics, serializers
from django_msgspec_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 msgspec 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_msgspec_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
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])
Global Settings
You can configure default enc_hook and dec_hook functions that will be used across all SchemaField instances when no custom hook is explicitly provided. This is useful for handling custom types globally without repeating the hook configuration on every field.
Add the DJANGO_MSGSPEC_FIELD setting to your Django settings.py:
# settings.py
def my_enc_hook(obj):
"""Custom encoder hook for unsupported types."""
if isinstance(obj, MyCustomType):
return obj.to_dict()
raise NotImplementedError(f"Cannot encode {type(obj)}")
def my_dec_hook(type_, obj):
"""Custom decoder hook for unsupported types."""
if type_ is MyCustomType:
return MyCustomType.from_dict(obj)
raise NotImplementedError(f"Cannot decode {type_}")
DJANGO_MSGSPEC_FIELD = {
"ENC_HOOK": my_enc_hook,
"DEC_HOOK": my_dec_hook,
}
You can also use dotted paths to reference functions defined elsewhere:
# settings.py
DJANGO_MSGSPEC_FIELD = {
"ENC_HOOK": "myapp.hooks.my_enc_hook",
"DEC_HOOK": "myapp.hooks.my_dec_hook",
}
Available Settings
| Setting | Type | Default | Description |
|---|---|---|---|
ENC_HOOK |
Callable[[Any], Any] or str |
None |
Default encoder hook for serializing unsupported types. Called when msgspec encounters a type it cannot serialize natively. |
DEC_HOOK |
Callable[[type, Any], Any] or str |
None |
Default decoder hook for deserializing custom types. Called when msgspec encounters a type it cannot deserialize natively. |
Overriding Global Settings
You can always override the global settings on individual fields by passing enc_hook or dec_hook directly:
from django_msgspec_field import SchemaField
def custom_enc_hook(obj):
# Field-specific encoding logic
...
class MyModel(models.Model):
# Uses global enc_hook/dec_hook from settings
data: MyStruct = SchemaField()
# Uses custom enc_hook, overriding the global setting
custom_data: MyStruct = SchemaField(enc_hook=custom_enc_hook)
Migrating from django-pydantic-field
If you're migrating from django-pydantic-field, here are the key changes:
-
Replace imports:
# Before (pydantic) from django_pydantic_field import SchemaField import pydantic class Foo(pydantic.BaseModel): count: int # After (msgspec) from django_msgspec_field import SchemaField import msgspec class Foo(msgspec.Struct): count: int
-
Schema definitions: Replace
pydantic.BaseModelwithmsgspec.Struct -
Config options: Remove
pydantic.ConfigDict- msgspec uses different configuration methods -
Validators: Replace Pydantic validators with msgspec constraints using
msgspec.Meta
Contributing
To get django-msgspec-field up and running in development mode:
- Clone this repo:
git clone https://github.com/quertenmont/django-msgspec-field.git - Install uv:
curl -LsSf https://astral.sh/uv/install.sh | sh - Install dependencies:
uv sync --all-extras - Setup
pre-commit:pre-commit install - Run tests:
uv run pytest - Run linting:
uv run ruff check . - Run type checking:
uv run mypy django_msgspec_field
License
Released under MIT License.
Supporting
- :star: Star this project on GitHub
- :octocat: Follow me on GitHub
- :blue_heart: Follow me on Twitter
- :moneybag: Sponsor me on Github
You can also support me via:
Acknowledgement
- Savva Surenkov for the original django-pydantic-field library
- Jim Crist-Harif for creating msgspec
- Churkin Oleg for his Gist as a source of inspiration
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_msgspec_field-0.1.12.tar.gz.
File metadata
- Download URL: django_msgspec_field-0.1.12.tar.gz
- Upload date:
- Size: 21.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
41e768360b1db3d72ab6cda340efeee10be45c0775779632be58ce8622732dba
|
|
| MD5 |
d55b0cbd557886da7fc182b70b368a9b
|
|
| BLAKE2b-256 |
37eb437601be7aec07b6b079d5981c2f67ff856db62097d31ac1289c3943eb5b
|
Provenance
The following attestation bundles were made for django_msgspec_field-0.1.12.tar.gz:
Publisher:
python-publish.yml on quertenmont/django-msgspec-field
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_msgspec_field-0.1.12.tar.gz -
Subject digest:
41e768360b1db3d72ab6cda340efeee10be45c0775779632be58ce8622732dba - Sigstore transparency entry: 1213483725
- Sigstore integration time:
-
Permalink:
quertenmont/django-msgspec-field@5157dfa6dd597e46bd2bb9f298b0cb14ed9f25b2 -
Branch / Tag:
refs/tags/0.1.12 - Owner: https://github.com/quertenmont
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@5157dfa6dd597e46bd2bb9f298b0cb14ed9f25b2 -
Trigger Event:
release
-
Statement type:
File details
Details for the file django_msgspec_field-0.1.12-py3-none-any.whl.
File metadata
- Download URL: django_msgspec_field-0.1.12-py3-none-any.whl
- Upload date:
- Size: 30.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2720cfba4532670428cbc6f80003ac364fc6bf0a3fc44d16c55b696320bd2a83
|
|
| MD5 |
b8015864ec4458f3541c0a9c04168fcb
|
|
| BLAKE2b-256 |
08ee6db6b148c17ee65d8f18609c5433ff912f4f8ead2468398d29fae9e7e9f2
|
Provenance
The following attestation bundles were made for django_msgspec_field-0.1.12-py3-none-any.whl:
Publisher:
python-publish.yml on quertenmont/django-msgspec-field
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_msgspec_field-0.1.12-py3-none-any.whl -
Subject digest:
2720cfba4532670428cbc6f80003ac364fc6bf0a3fc44d16c55b696320bd2a83 - Sigstore transparency entry: 1213483761
- Sigstore integration time:
-
Permalink:
quertenmont/django-msgspec-field@5157dfa6dd597e46bd2bb9f298b0cb14ed9f25b2 -
Branch / Tag:
refs/tags/0.1.12 - Owner: https://github.com/quertenmont
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@5157dfa6dd597e46bd2bb9f298b0cb14ed9f25b2 -
Trigger Event:
release
-
Statement type: