Django JSON Schema Editor¶
A powerful Django widget for integrating @json-editor/json-editor with Django forms and admin interfaces. It provides a rich, schema-based editing experience for JSON data in Django applications.
See the blog post for the announcement and a screenshot.
Features¶
Schema-based validation for JSON data
Django admin integration
Rich text editing capabilities with optional prose editor
Foreign key references with Django admin lookups
Referential integrity for JSON data containing model references
JSON Schema Support¶
The widget supports the JSON Schema standard for defining the structure and validation rules of your JSON data. Notable supported features include:
Basic types: string, number, integer, boolean, array, object
Format validations: date, time, email, etc.
Custom formats: prose (rich text), foreign_key (model references)
Required properties
Enums and default values
Nested objects and arrays
The documentation for the json-editor offers a good overview over all supported features.
Installation¶
pip install django-json-schema-editor
For django-prose-editor support (rich text editing):
pip install django-json-schema-editor[prose]
Usage¶
Basic Setup¶
Add
django_json_schema_editorto yourINSTALLED_APPS:INSTALLED_APPS = [ # ... 'django_json_schema_editor', # ... ]
Use the
JSONFieldin your models:from django.db import models from django_json_schema_editor.fields import JSONField class MyModel(models.Model): data = JSONField( schema={ "type": "object", "properties": { "title": {"type": "string"}, "description": {"type": "string"}, "count": {"type": "integer"}, }, "required": ["title", "description", "count"], } )
Note! required contains a list of properties which should exist in the
JSON blob. The values themselves do not have to be truthy. The advantage of
always specifying required is that the properties are automatically shown
also when editing data which was added when those properties didn’t all exist
yet.
Rich Text Editing¶
For rich text editing, use the prose format:
class MyModel(models.Model):
data = JSONField(
schema={
"type": "object",
"properties": {
"title": {"type": "string"},
"content": {"type": "string", "format": "prose"},
},
"required": ["title", "content"],
}
)
Configuring Prose Editor Extensions¶
You can customize which formatting options are available in the prose editor by specifying extensions in the field options:
class MyModel(models.Model):
data = JSONField(
schema={
"type": "object",
"properties": {
"title": {"type": "string"},
"content": {
"type": "string",
"format": "prose",
"options": {
"extensions": {
"Bold": True,
"Italic": True,
# Only Bold and Italic will be available
# (core extensions are always included)
}
}
},
},
"required": ["title", "content"],
}
)
The prose editor always includes core extensions (Document, Paragraph, HardBreak, Text, Menu). By default, it also includes Bold, Italic, Underline, Subscript, and Superscript extensions. When you specify custom extensions, only the core extensions plus your specified extensions will be active.
Foreign Key References¶
You can reference Django models in your JSON data:
class MyModel(models.Model):
data = JSONField(
schema={
"type": "object",
"properties": {
"title": {"type": "string"},
"image": {
"type": "string",
"format": "foreign_key",
"options": {
"url": "/admin/myapp/image/?_popup=1&_to_field=id",
},
},
},
"required": ["title", "image"],
}
)
Displaying Foreign Key Labels¶
By default, foreign key fields only store the primary key value. To display human-readable labels in the admin interface, use the foreign_key_descriptions parameter:
class Article(models.Model):
data = JSONField(
schema={
"type": "object",
"properties": {
"title": {"type": "string"},
"featured_image": {
"type": "string",
"format": "foreign_key",
"options": {
"url": "/admin/myapp/image/?_popup=1&_to_field=id",
},
},
"gallery": {
"type": "array",
"items": {
"type": "string",
"format": "foreign_key",
"options": {
"url": "/admin/myapp/image/?_popup=1&_to_field=id",
},
},
},
},
"required": ["title"],
},
foreign_key_descriptions=[
("myapp.image", lambda data: [
pk for pk in [data.get("featured_image")] + data.get("gallery", []) if pk
]),
],
)
The foreign_key_descriptions parameter accepts a list of tuples, where each tuple contains:
Model label (string): The model’s app label and model name in the format
"app_label.model_name"Getter function: A callable that takes the JSON data and returns a list of primary keys to resolve
Important: The getter function must always return a list of primary keys (even for single foreign key values), which will be resolved to display strings in the admin interface.
Data References and Referential Integrity¶
One of the most powerful features is the ability to maintain referential integrity between JSON data and model instances. This prevents referenced objects from being deleted while they’re still in use.
Basic Usage with JSONField¶
For regular Django models using JSONField, you can manually register data references:
from django.db import models
from django_json_schema_editor.fields import JSONField
class Image(models.Model):
title = models.CharField(max_length=100)
file = models.FileField(upload_to='images/')
class Article(models.Model):
data = JSONField(
schema={
"type": "object",
"properties": {
"title": {"type": "string"},
"content": {"type": "string", "format": "prose"},
"featured_image": {
"type": "string",
"format": "foreign_key",
"options": {
"url": "/admin/myapp/image/?_popup=1&_to_field=id",
},
},
},
"required": ["title", "content", "featured_image"],
}
)
def get_image_ids(article):
if image_id := article.data.get("featured_image"):
return [int(image_id)]
return []
# Register the reference to prevent images from being deleted when they're referenced
Article.register_data_reference(
Image,
name="featured_images",
getter=get_image_ids,
)
This prevents a referenced image from being deleted as long as it’s referenced in an article’s JSON data.
The name field will be the name of the underlying ManyToManyField which actually references the Image instances.
Important: The get_image_ids getter must be written defensively – you cannot assume the model is valid. For example, you cannot assume that foreign key values are set (even when they are null=False). Django’s validation hasn’t cleared the model before the getter is invoked for the first time.
You can use the paths_to_pks utility also; the get_image_ids implementation using it would look like this:
from django_json_schema_editor.fields import paths_to_pks
def get_image_ids(article):
return paths_to_pks(to=Image, paths=["featured_image"], data=article.data)
Streamlined Approach with foreign_key_paths¶
For JSON plugins (see the feincms3 section below), the foreign_key_paths parameter provides a more declarative way to achieve the same result without writing manual getter functions. Instead of extracting values with custom Python code, you specify JMESPath expressions that locate foreign keys in your JSON structure.
feincms3 JSON Plugin Support¶
Django JSON Schema Editor provides enhanced support for feincms3 JSON plugins with self-describing capabilities using jmespath values. This allows for more intelligent display names and better integration with feincms3’s plugin system.
Self-Describing JSON Plugins¶
When using the jmespath dependency, you can define schemas that describe how to extract display values from the JSON data:
from django_json_schema_editor.plugins import JSONPluginBase
class TextPlugin(JSONPluginBase):
SCHEMA = {
"type": "object",
"title": "Text Block",
"__str__": "title", # jmespath to extract display value
"properties": {
"title": {
"type": "string",
"title": "Title",
},
"content": {
"type": "string",
"format": "prose",
"title": "Content",
},
},
"required": ["title", "content"],
}
With this setup:
Display Names: The
__str__method will use the jmespath (title) to extract a display value from the plugin’s dataFallback Behavior: If the jmespath fails or the value is empty, it falls back to the schema’s
titlefieldDefault Fallback: As a last resort, it falls back to the standard plugin type name
This feature makes feincms3 plugin instances much more readable in the admin interface and throughout your application.
Foreign Key References in JSON Plugins¶
When your JSON plugins contain foreign key references, you can use foreign_key_paths to streamline the configuration:
from django_json_schema_editor.plugins import JSONPluginBase
class VocabularyPlugin(JSONPluginBase):
SCHEMA = {
"type": "object",
"title": "Vocabulary Table",
"__str__": "title",
"properties": {
"title": {"type": "string"},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"word": {"type": "string"},
"audiofile": {
"type": "string",
"format": "foreign_key",
"options": {
"url": "/admin/files/file/?_popup=1&_to_field=id",
"model": "files.file",
},
},
"example": {"type": "string"},
"example_audiofile": {
"type": "string",
"format": "foreign_key",
"options": {
"url": "/admin/files/file/?_popup=1&_to_field=id",
"model": "files.file",
},
},
},
},
},
},
}
# Create the proxy plugin with foreign_key_paths
VocabularyTablePlugin = JSONPluginBase.proxy(
"vocabulary_table",
verbose_name="Vocabulary Table",
schema=SCHEMA,
foreign_key_paths={
"files.file": ["items[*].audiofile", "items[*].example_audiofile"],
},
)
The foreign_key_paths parameter:
Key: Model label in the format
"app_label.model_name"Value: List of JMESPath expressions that locate foreign key values in the JSON data
Supports complex paths including array wildcards (
[*]) for nested structures
This provides two main benefits:
Referential Integrity: When combined with
register_foreign_key_reference(), it automatically prevents referenced models from being deleted while they’re in useAdmin Display: In the admin interface (via
JSONPluginInline), foreign key values are automatically resolved to display human-readable labels
Setting Up Referential Integrity¶
After defining your plugin proxy classes, register them to maintain referential integrity:
# Register foreign key relationships for all plugins
ChapterStructuredData.register_foreign_key_reference(
File, # The model being referenced
name="referenced_files", # Name for the M2M relationship
)
This will automatically:
Extract foreign key values from your JSON data using the
foreign_key_pathsdefined in each pluginCreate many-to-many relationships to track these references
Prevent deletion of referenced models when they’re in use
The foreign_key_paths approach is more maintainable than manually writing getter functions, especially when dealing with nested arrays or multiple foreign key fields in your JSON schema.
Extending Proxy Plugins with Mixins¶
When creating proxy plugins with JSONPluginBase.proxy(), you can add custom functionality using the mixins parameter:
from django_json_schema_editor.plugins import JSONPluginBase
class RenderMixin:
def render(self):
"""Custom rendering logic for this plugin type."""
return f"<div>{self.data.get('content', '')}</div>"
TextPlugin = JSONPluginBase.proxy(
"text",
verbose_name="Text Block",
schema={
"type": "object",
"properties": {
"content": {"type": "string", "format": "prose"},
},
},
mixins=[RenderMixin],
)
Why mixins instead of subclassing? Proxy plugins created with .proxy() are automatically registered in an internal map. This registration enables the QuerySet’s .downcast() method to return the correct proxy class for each plugin type. If you were to subclass the returned proxy further, those subclasses wouldn’t be registered, and .downcast() would return the base proxy instead of your extended version.
The mixins parameter accepts a list or tuple of mixin classes that will be added to the proxy’s method resolution order (MRO), allowing you to:
Add custom methods and properties
Override base class behavior
Share functionality across multiple plugin types
Development¶
To set up the development environment:
Clone the repository
Install development dependencies:
pip install -e ".[tests,prose]"
Code Quality¶
This project uses several tools to maintain code quality:
pre-commit: We use pre-commit hooks to ensure consistent code style and quality
ruff: For Python linting and formatting
biome: For JavaScript and CSS linting and formatting
To set up pre-commit:
uv tool install pre-commit
pre-commit install
The pre-commit configuration includes:
Basic file checks (trailing whitespace, merge conflicts, etc.)
Django upgrade checks
Ruff for Python linting and formatting
Biome for JavaScript and CSS linting and formatting
pyproject.toml validations
Running Tests¶
pytest
Contributing¶
Contributions are welcome! Please feel free to submit a Pull Request.
License¶
This project is licensed under the MIT License.
Change log¶
Next version¶
Fixed the material icons icon size to only apply to JSON editor buttons.
0.12 (2025-12-04)¶
Added the material icons library and added the iconlib integration for it to the JSON editor by default.
Added official compatibility with Django 6.0, dropped 5.1 from CI (we still support 4.2).
0.11 (2025-11-19)¶
Backwards incompatible: Changed the
JSONPluginBase.proxyclassmethod signature. The**class_dictparameter (introduced in 0.10) has been replaced with amixinsparameter that accepts a list or tuple of mixin classes. This provides a cleaner, more explicit pattern for extending proxy plugin functionality.Before (0.10 only):
MyPlugin = JSONPluginBase.proxy( "my_plugin", schema={...}, custom_method=lambda self: "value", # Added to class body )
After:
class MyMixin: def custom_method(self): return "value" MyPlugin = JSONPluginBase.proxy( "my_plugin", schema={...}, mixins=[MyMixin], )
The
metaparameter (introduced in 0.10) continues to work for adding attributes to theMetaclass. Theverbose_nameparameter also continues to work as before.
0.10 (2025-11-17)¶
Cleaned up the implementation, added
paths_to_pksto the fields module for reuse.Backwards incompatible: Changed the
JSONPluginBase.proxyclassmethod to add keyword arguments passed to it to the class and not to theMetaclass. To add to theMetaclass, use themetakeyword argument. Theverbose_nameargument which is also used in the documentation continues to work.
0.9 (2025-11-13)¶
Backwards incompatible change to ``JSONPluginBase`` The JSON pointer usage has been replaced by jmespath usage.
__str__values in schemas for theJSONPluginBaseshould be updated for the new syntax (for example,object.titleinstead of/object/title), otherwise the code acts as if they weren’t there. Failures are silent because crashes aren’t worth it. Removed thejsonpointerextra and instead added a hard dependency onjmespath.Be more careful in
JSONPluginBase.__str__to actually return strings.Allowed forwarding more config options to the configurable prose editor, not just extensions.
Added a more streamlined way to use foreign key references when using the
JSONPluginBase, seeforeign_key_pathsandregister_foreign_key_referencein the documentation. The new functionality allows preventing deletion of referenced objects and showing a description of the referenced object (like Django’s ownraw_id_fields) with less steps.
0.8 (2025-09-25)¶
Fixed the initialization of the JSON editor when we start with null values.
Changed the ID attributes of generated elements to hopefully be unique by using a unique
form_name_rootper editor instance.Added optional support for self-describing JSON plugins using JSON pointers.
0.7 (2025-09-05)¶
Reverted the
required_by_defaultchange, it was bad (tm) because it didn’t allow removing existing properties at all. Better learn setting therequiredproperties explicitly!
0.6 (2025-09-05)¶
Added validation for foreign key references to provide meaningful error messages instead of server crashes when invalid primary keys are entered.
Fixed the error where edits could be lost by automatically dispatching
changeevents when seeinginputevents to trigger the JSON editor’sonChangeupdates.
0.5 (2025-06-26)¶
Fixed the size of checkboxes in the JSON editor.
Added configurable extensions support for the prose editor, allowing customization of available formatting options.
0.4 (2025-03-24)¶
Added a simple e2e test suite.
Improved the prose editor integration, added the required importmap dependency.
Expanded the README a lot.
0.3 (2025-03-20)¶
Fixed the JSON plugin data reference handling.
Added a
[prose]extra depending on the newest alpha version of django-prose-editor.
0.2 (2024-12-04)¶
Included the django-prose-editor support by default, it’s a small file without much impact as long as the editor itself isn’t loaded. The minimum supported version of django-prose-editor is 0.10a5.
Updated the JSON editor to 2.15.2.
0.1 (2024-08-02)¶
Initial beta release.