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"]
}

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">
  &lt;model xmlns:form="http://namespaces.plone.org/supermodel/form"
         xmlns="http://namespaces.plone.org/supermodel/schema"&gt;
    &lt;schema&gt;
      &lt;field name="linklist" type="plone.schema.jsonfield.JSONField"&gt;
        &lt;title&gt;Link List&lt;/title&gt;
        &lt;description/&gt;
        &lt;required&gt;False&lt;/required&gt;
        &lt;default&gt;{'items': []}&lt;/default&gt;
        &lt;schema&gt;{
          "type": "object",
          "properties": {
            "items": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "title": {"type": "string", "title": "Title"},
                  "url": {"type": "string", "title": "URL", "format": "link"}
                }
              }
            }
          }
        }&lt;/schema&gt;
        &lt;form:widget type="wcs.backend.widgets.jsonschema_widget.JSONSchemaWidget"/&gt;
      &lt;/field&gt;
    &lt;/schema&gt;
  &lt;/model&gt;
</property>

With maxItems:

<property name="model_source">
  &lt;model xmlns:form="http://namespaces.plone.org/supermodel/form"
         xmlns="http://namespaces.plone.org/supermodel/schema"&gt;
    &lt;schema&gt;
      &lt;field name="highlights" type="plone.schema.jsonfield.JSONField"&gt;
        &lt;title&gt;Highlights&lt;/title&gt;
        &lt;required&gt;False&lt;/required&gt;
        &lt;default&gt;{'items': []}&lt;/default&gt;
        &lt;schema&gt;{
          "type": "object",
          "properties": {
            "items": {
              "type": "array",
              "maxItems": 3,
              "items": {
                "type": "object",
                "properties": {
                  "title": {"type": "string", "title": "Title"},
                  "url": {"type": "string", "title": "URL", "format": "link"}
                }
              }
            }
          }
        }&lt;/schema&gt;
        &lt;form:widget type="wcs.backend.widgets.jsonschema_widget.JSONSchemaWidget"/&gt;
      &lt;/field&gt;
    &lt;/schema&gt;
  &lt;/model&gt;
</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 form XML 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'])