# 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": []} ```