# 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
```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.
```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:
```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.
```json
{"type": "string", "title": "Label"}
```
### Date
Renders an HTML5 date picker.
```json
{"type": "string", "format": "date", "title": "Start Date"}
```
### Time
Renders an HTML5 time picker.
```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:**
```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.
```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:
```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.
```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.
```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.
```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:**
```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:**
```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.
```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):**
```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):**
```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:**
```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:**
```json
{"items": []}
```