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.
- 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
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.
pip install django-json-schema-editorFor django-prose-editor support (rich text editing):
pip install django-json-schema-editor[prose]- 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.
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"],
}
)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.
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"],
}
)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.
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.
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)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.
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.
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 data - Fallback Behavior: If the jmespath fails or the value is empty, it falls back to the schema's
titlefield - Default 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.
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 use - Admin Display: In the admin interface (via
JSONPluginInline), foreign key values are automatically resolved to display human-readable labels
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 plugin - Create 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.
To set up the development environment:
- Clone the repository
- Install development dependencies:
pip install -e ".[tests,prose]"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 installThe 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
pytestContributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License.