JSON Schema Widget¶
The JSON Schema Widget provides a dynamic, repeatable form interface for managing structured JSON data on Dexterity content types. It renders an interactive Vue.js application that allows editors to add, remove, reorder, and edit entries within a list, all driven by a JSON Schema definition.
The widget is built on plone.schema.JSONField and replaces the default textarea
with a rich editing experience that supports multiple field formats including text,
date, time, select dropdowns, and internal/external links.
Schema Structure¶
The widget expects a JSON Schema with a specific top-level structure. The root
must be an object with a properties key. Within properties, the main
iterable key (by convention items) defines the repeatable array, while any
other top-level properties become metadata fields displayed above the list.
Minimal Schema¶
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": {"type": "string", "title": "Title"}
}
}
}
}
}
This produces a repeatable list where each entry has a single text field called “Title”.
Schema with Metadata¶
Properties defined alongside items at the top level become metadata fields.
Metadata fields are rendered once above the list and are not repeated per entry.
{
"type": "object",
"properties": {
"category": {"type": "string", "title": "Category"},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string", "title": "Name"}
}
}
}
}
}
Stored Data Format¶
Data is stored as a JSON object matching the schema structure:
{
"category": "Main links",
"items": [
{"name": "First entry"},
{"name": "Second entry"}
]
}
The default value for the field must match this structure. For a field with
only an items array, use {"items": []}.
Supported Field Formats¶
Each property within the array items is rendered as a specific input component
based on its type and format attributes.
Text (default)¶
Any string field without a specific format renders as a standard text input.
{"type": "string", "title": "Label"}
Date¶
Renders an HTML5 date picker.
{"type": "string", "format": "date", "title": "Start Date"}
Time¶
Renders an HTML5 time picker.
{"type": "string", "format": "time", "title": "Opens at", "default": "08:00"}
Select (Dropdown / Multi-select)¶
Renders a dropdown or multi-select depending on the field type. The vocabulary
attribute provides the list of available options.
Single select:
{
"type": "string",
"format": "select",
"title": "Priority",
"vocabulary": ["Low", "Medium", "High"]
}
Multi-select (array type):
When type is "array", the select renders as a multi-select list.
{
"type": "array",
"format": "select",
"title": "Days of week",
"default": ["Monday"],
"vocabulary": [
"Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday", "Sunday"
]
}
Custom value mapping:
Vocabulary entries support a title|value syntax to separate the display label
from the stored value:
{
"type": "string",
"format": "select",
"vocabulary": ["German|de", "French|fr", "Italian|it"]
}
Link¶
Renders a link input with three tabs: internal (Plone content browser),
external URL, and email. Internal links are stored as resolveuid references
and automatically resolved to absolute URLs by the REST API serializer.
{"type": "string", "format": "link", "title": "URL"}
Limiting the Number of Entries (maxItems)¶
The maxItems property on the array definition limits how many entries an editor
can add. When the maximum is reached, the “Add” button is hidden.
{
"type": "object",
"properties": {
"items": {
"type": "array",
"maxItems": 5,
"items": {
"type": "object",
"properties": {
"title": {"type": "string", "title": "Title"},
"url": {"type": "string", "format": "link", "title": "URL"}
}
}
}
}
}
With "maxItems": 5, the widget allows at most 5 entries. Once 5 entries exist,
the “Add” button disappears. Deleting an entry brings it back.
This is a frontend-only constraint enforced by the Vue.js widget. For
server-side validation of minItems and maxItems, use standard JSON Schema
validation with the jsonschema library in a custom validator.
Default Values¶
Each property within items can specify a default value. When an editor
clicks “Add”, a new entry is created with these defaults pre-filled.
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"@type": {"type": "string", "default": "LinkItem"},
"title": {"type": "string", "title": "Title"},
"active": {"type": "string", "default": "yes"}
}
}
}
}
}
Fields without an explicit default are initialised to "" (empty string), or
[] (empty array) if the field type is "array".
Configuration¶
FTI XML (model_source)¶
The most common way to configure the widget is through model_source in an FTI
XML profile. This approach is used for content types whose schema is defined
entirely in XML rather than in a Python interface.
Basic link list:
<property name="model_source">
<model xmlns:form="http://namespaces.plone.org/supermodel/form"
xmlns="http://namespaces.plone.org/supermodel/schema">
<schema>
<field name="linklist" type="plone.schema.jsonfield.JSONField">
<title>Link List</title>
<description/>
<required>False</required>
<default>{'items': []}</default>
<schema>{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": {"type": "string", "title": "Title"},
"url": {"type": "string", "title": "URL", "format": "link"}
}
}
}
}
}</schema>
<form:widget type="wcs.backend.widgets.jsonschema_widget.JSONSchemaWidget"/>
</field>
</schema>
</model>
</property>
With maxItems:
<property name="model_source">
<model xmlns:form="http://namespaces.plone.org/supermodel/form"
xmlns="http://namespaces.plone.org/supermodel/schema">
<schema>
<field name="highlights" type="plone.schema.jsonfield.JSONField">
<title>Highlights</title>
<required>False</required>
<default>{'items': []}</default>
<schema>{
"type": "object",
"properties": {
"items": {
"type": "array",
"maxItems": 3,
"items": {
"type": "object",
"properties": {
"title": {"type": "string", "title": "Title"},
"url": {"type": "string", "title": "URL", "format": "link"}
}
}
}
}
}</schema>
<form:widget type="wcs.backend.widgets.jsonschema_widget.JSONSchemaWidget"/>
</field>
</schema>
</model>
</property>
Key points for FTI XML configuration:
The field type is always
plone.schema.jsonfield.JSONField.The
<schema>element contains the JSON schema as a JSON string.The
<default>element must contain valid Python dict/list syntax (it is evaluated by Plone’s supermodel handler).The widget is assigned via
<form:widget type="..."/>.The
formXML namespace must be declared:xmlns:form="http://namespaces.plone.org/supermodel/form"
Python (Interface definition)¶
When defining a content type schema in Python, use plone.autoform.directives
to assign the widget and plone.schema.JSONField for the field.
from plone.autoform import directives
from plone.schema import JSONField
from plone.supermodel import model
from wcs.backend.widgets.jsonschema_widget import JSONSchemaWidget
import json
LINK_LIST_SCHEMA = json.dumps({
"type": "object",
"properties": {
"items": {
"type": "array",
"maxItems": 10,
"items": {
"type": "object",
"properties": {
"header_name": {"type": "string", "title": "Name"},
"header_value": {"type": "string", "title": "Value"},
}
}
}
}
})
class IMyContentType(model.Schema):
directives.widget('my_field', JSONSchemaWidget)
my_field = JSONField(
title=u'My Field',
schema=LINK_LIST_SCHEMA,
default={"items": []},
required=False,
)
The schema parameter on JSONField expects a JSON string (not a dict). Use
json.dumps() when defining the schema in Python.
REST API Serialization¶
The widget stores data as a native Python dict/list on the content object. The
custom JSONFieldSerializer in wcs.backend.restapi.fields handles serialization
and automatically resolves resolveuid references in link fields to absolute URLs
based on the format: "link" marker in the JSON schema.
Fetching data (JavaScript):
const response = await fetch('/plone/my-content/@linklist', {
headers: {'Accept': 'application/json'}
});
const data = await response.json();
// data.items -> [{title: "...", url: "https://..."}, ...]
Fetching data (Python requests):
import requests
response = requests.get(
'https://example.com/my-content',
headers={'Accept': 'application/json'}
)
data = response.json()
linklist = data.get('linklist', {})
for entry in linklist.get('items', []):
print(entry['title'], entry['url'])