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