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
~~~~~~~~~~~~~~
.. code-block:: json
{
"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.
.. code-block:: json
{
"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:
.. code-block:: json
{
"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.
.. code-block:: json
{"type": "string", "title": "Label"}
Date
~~~~
Renders an HTML5 date picker.
.. code-block:: json
{"type": "string", "format": "date", "title": "Start Date"}
Time
~~~~
Renders an HTML5 time picker.
.. code-block:: json
{"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:**
.. code-block:: json
{
"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.
.. code-block:: json
{
"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:
.. code-block:: json
{
"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.
.. code-block:: json
{"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.
.. code-block:: json
{
"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.
.. code-block:: json
{
"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:**
.. code-block:: xml
<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>
**With maxItems:**
.. code-block:: xml
<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>
Key points for FTI XML configuration:
- The field type is always ``plone.schema.jsonfield.JSONField``.
- The ```` element contains the JSON schema as a JSON string.
- The ```` element must contain valid Python dict/list syntax (it is
evaluated by Plone's supermodel handler).
- The widget is assigned via ````.
- 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.
.. code-block:: python
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):**
.. code-block:: 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):**
.. code-block:: python
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'])
Complete Example: Social Links with Icon and maxItems
-----------------------------------------------------
This example defines a social media links field limited to 6 entries, where each
entry has a title, an icon name, and a URL.
**JSON Schema:**
.. code-block:: json
{
"type": "object",
"properties": {
"items": {
"type": "array",
"maxItems": 6,
"items": {
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Title"
},
"url": {
"type": "string",
"title": "URL",
"format": "link"
},
"icon": {
"type": "string",
"title": "Icon (CSS class name)"
}
}
}
}
}
}
**Default value:**
.. code-block:: json
{"items": []}