Skip to main content

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

PyPI Python versions License

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

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

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

django_modern_schemas-0.1.0.tar.gz (18.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_modern_schemas-0.1.0-py3-none-any.whl (24.5 kB view details)

Uploaded Python 3

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

Hashes for django_modern_schemas-0.1.0.tar.gz
Algorithm Hash digest
SHA256 9a9a6d0834bf63edbd611947df950637c2c3e635fb2eeb09569441df72c2fc33
MD5 056b33bdbb0372cd7e4214dbdc0b3244
BLAKE2b-256 3b72c9a2659952307548cc23f15eda3c223e591a2b7cd11d97edbaa874b669e0

See more details on using hashes here.

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

Hashes for django_modern_schemas-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0ba3ddb60cf58bf28ea6d32a67891a397ee64baeda910bbc682971c8babf61df
MD5 f268b86deecdc884903d213a0cf2550e
BLAKE2b-256 1a26497ae3d9d79c8201f98cf7448b2532fd862e3bb5bb4dd8a9c5ca8524665a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page