Django Modern Schemas
django-modern-schemas generates Pydantic schemas from Django ORM models. It
reuses model types, constraints, defaults, choices, and supported relationships
to reduce duplication between the data and validation/serialization layers. The
library exposes ModelSchema, Schema, and SchemaFactory to define or
generate these schemas. It is maintained by
Open Byte.
📚 Full documentation: open-byte.github.io/django-modern-schemas
Requirements
- Python 3.10 or newer
- Django 3.2 or newer
- Pydantic 2.12 or newer
Quick start
pip install django-modern-schemas
Nothing goes into INSTALLED_APPS — schemas are ordinary Python classes.
Start from a model you already have — models.py:
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
class Article(models.Model):
title = models.CharField(max_length=120)
body = models.TextField(blank=True, default='')
views = models.PositiveIntegerField(default=0)
published = models.BooleanField(default=False)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
Point a schema at it. The fields, their types, their constraints, and their
defaults are read from the model — you restate none of them in schemas.py:
from django_modern_schemas import ModelSchema
from .models import Article
class ArticleSchema(ModelSchema[Article]):
class Config:
model = Article
ModelSchema is generic in its model. The parameter is free at runtime and
makes create(), update(), and save() typed as returning Article, so your
type checker follows the value all the way into the rest of the view.
>>> list(ArticleSchema.model_fields)
['id', 'title', 'body', 'views', 'published', 'author']
Validate input
max_length=120 was declared once, on the column, and it is enforced before any
SQL runs. ValidationError.errors() is already shaped like a 400 response body:
>>> ArticleSchema.model_validate({'title': 'x' * 200, 'author': 1})
Traceback (most recent call last):
...
pydantic_core._pydantic_core.ValidationError: 1 validation error for ArticleSchema
title
String should have at most 120 characters [type=string_too_long, ...]
Write it to the database
>>> author = Author.objects.create(name='Ada Lovelace')
>>> article = ArticleSchema.model_validate(
... {'title': 'Schemas from models', 'body': 'One source of truth.', 'author': author.pk}
... ).create()
>>> article.pk is not None
True
Serialize it back out
The same class reads a Django instance, so one schema covers both directions:
>>> ArticleSchema.model_validate(article).model_dump()
{'id': 1, 'title': 'Schemas from models', 'body': 'One source of truth.', 'views': 0, 'published': False, 'author': 1}
>>> ArticleSchema.model_validate(article).model_dump_json()
'{"id":1,"title":"Schemas from models","body":"One source of truth.","views":0,"published":false,"author":1}'
Round trip: read, edit, save
A schema validated from an instance stays bound to it, so save() updates that
row instead of inserting a new one — no bookkeeping on your side:
>>> schema = ArticleSchema.model_validate(article)
>>> schema.title = 'Schemas from models, revisited'
>>> saved = schema.save()
>>> saved.pk == article.pk
True
>>> Article.objects.get(pk=article.pk).title
'Schemas from models, revisited'
>>> Article.objects.count() # updated in place, not duplicated
1
PATCH endpoints
Mark fields optional and update with partial=True, and keys the client never
sent are never written:
class ArticlePatchSchema(ModelSchema[Article]):
class Config:
model = Article
fields = ['title', 'published']
optional = ['title', 'published']
>>> Article.objects.filter(pk=article.pk).update(views=42) # the article got some traffic
1
>>> article.refresh_from_db()
>>> ArticlePatchSchema.model_validate({'published': True}).update(article, partial=True).published
True
>>> Article.objects.get(pk=article.pk).views # untouched by the patch
42
Without partial=True that same payload would write views back to its default
and undo the count.
Publish the contract
model_json_schema() hands OpenAPI tooling a description generated from the
model, maxLength and defaults included:
>>> ArticleSchema.model_json_schema()['required']
['title', 'author']
>>> ArticleSchema.model_json_schema()['properties']['title']['maxLength']
120
From here: Getting Started
walks the same ground in more detail, and
Relations
covers foreign keys, many-to-many, and nesting with depth.
Documentation
The documentation site is published at open-byte.github.io/django-modern-schemas. It is built with Material for MkDocs, its source lives in docs, and every Python example on it is executed by the test suite — a drifting example fails the build.
uv sync --group docs
uv run --group docs mkdocs serve
- Getting Started
- Overview
- ModelSchema Guide
- Relations Guide
- Persistence Guide
- Source and MethodSource Guide
- Configuration Reference
- Credits and Stewardship
- Publishing
Schema configuration
model: the Django model used to build the schema.fields: fields exposed by the generated schema.exclude: fields to omit from the generated schema.optional: fields that should be optional.depth: the nesting depth for supported related models.
Tutorials
- Examples index: tested executable Python examples.
Credits and acknowledgements
Django Modern Schemas is maintained by Open Byte.
This project is a new evolution of Ninja Schema and is developed with the original creator's permission.
Special thanks and full recognition go to Tochukwu (@eadwinCode), the creator of Ninja Schema and Django Ninja Extra. Thank you for the effort, design, and work invested in both libraries, and for granting permission to modify and create this new implementation so that the idea can continue. The original work is credited to him.
Inspired by: Django Ninja and djantic.
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_modern_schemas-0.1.0.tar.gz.
File metadata
- Download URL: django_modern_schemas-0.1.0.tar.gz
- Upload date:
- Size: 18.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9a9a6d0834bf63edbd611947df950637c2c3e635fb2eeb09569441df72c2fc33
|
|
| MD5 |
056b33bdbb0372cd7e4214dbdc0b3244
|
|
| BLAKE2b-256 |
3b72c9a2659952307548cc23f15eda3c223e591a2b7cd11d97edbaa874b669e0
|
File details
Details for the file django_modern_schemas-0.1.0-py3-none-any.whl.
File metadata
- Download URL: django_modern_schemas-0.1.0-py3-none-any.whl
- Upload date:
- Size: 24.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0ba3ddb60cf58bf28ea6d32a67891a397ee64baeda910bbc682971c8babf61df
|
|
| MD5 |
f268b86deecdc884903d213a0cf2550e
|
|
| BLAKE2b-256 |
1a26497ae3d9d79c8201f98cf7448b2532fd862e3bb5bb4dd8a9c5ca8524665a
|