Custom renderer for patternProperties

I have a JSON schema that uses patternProperties frequently. I understand JSON Forms does not support pattern properties, but a custom renderer could be used in any instances patternProperties is identified.

I have tried to read up on guides for custom renderers and previous forum posts relating to patternProperties to create a solution, but I feel some guidance is needed. I am using react with the latest version of JSON Forms.

I have created basic JSON schemas and UISchemas to test with:

const jsonSchema = {
    type: 'object',
    patternProperties: {
      '^..*': {
        type: 'object',
        properties: {
          subField1: { type: 'string' },
          subField2: { type: 'integer' }
        }
      }
    }
  };
const uiSchema = {
    type: 'VerticalLayout',
    elements: [
      {
        type: 'Control',
        scope: '#/patternProperties/^..*',
        label: 'Custom Object',
        options: {
          detail: {
            type: 'VerticalLayout',
            elements: [
              {
                type: 'Control',
                scope: '#/properties/subField1',
                label: 'Sub Field 1'
              },
              {
                type: 'Control',
                scope: '#/properties/subField2',
                label: 'Sub Field 2'
              }
            ]
          }
        }
      }
    ]
  };

Heres JSONForms:

<JsonForms
                    schema={attachmentSchema}
                    uischema={uiSchema}
                    data={formData}
                    renderers={renderers}
                    cells={materialCells}
                    onChange={({ data }) => setFormData(data)}
                  />

And my tester:

const renderers = [
    ...materialRenderers,
    {
      tester: (uischema) => {
        return uischema && uischema.scope && uischema.scope.startsWith('#/patternProperties/')
          ? 3
          : -1;
      },
      renderer: PatternPropertiesRenderer
    }
  ];

What I want to happen, is for JSONForms to use built in renderer in most cases, but when patternProperties is identified, it uses a custom renderer that allows the user to input a key value, providing it is valid, the key is added as a title. Once added, I would like the built in renderers to take over and the form to continue to render as normal until another patternProperties is found.

My thought was to call my custom renderer, and then render JSONForms from within that. If patternProperties is then identified in my custom renderer, it can recursively call itself. It kind of works, in that I can add a key that matches the patternProperties and it renders the objects inside:

Screenshot 2024-07-12 at 10.10.18

However, I’ve noted that when I use the uischema prop from my custom renderer it wont work correctly, the above screenshots show the auto generated form. I can only add one picture, but if I use the uischema prop, it just lets me keep adding keys but no objects inside are loaded.

Heres my custom renderer code:

import React, { useState } from 'react';
import { withJsonFormsControlProps } from '@jsonforms/react';
import { Button, TextField } from '@mui/material';
import { JsonForms } from '@jsonforms/react';
import { materialRenderers, materialCells } from '@jsonforms/material-renderers';

const PatternPropertiesRenderer = ({ data = {}, handleChange, path, schema, uischema }) => {
  const [newFieldName, setNewFieldName] = useState('');

  const handleAddField = () => {
    if (newFieldName && newFieldName.match(path)) {
      handleChange(path, {
        ...data,
        [newFieldName]: ''
      });
      setNewFieldName('');
    } else {
      alert('Field name ' + newFieldName + ' must match one of the pattern properties');
    }
  };

  const handleFieldChange = (key, value) => {
    handleChange(path, { ...data, [key]: value });
  };

  const renderers = [
    ...materialRenderers,
    {
      tester: (uischema) => {
        return uischema && uischema.scope && uischema.scope.startsWith('#/patternProperties/')
          ? 3
          : -1;
      },
      renderer: PatternPropertiesRenderer
    }
  ];

  return (
    <div>
      {Object.keys(data).map((key) => {
        return (
          <div key={key}>
            <h4 className="text-black">{key}</h4>
            <JsonForms
              schema={schema}
              uischema={uischema}
              data={data[key]}
              renderers={renderers}
              cells={materialCells}
              onChange={({ data }) => handleFieldChange(key, data)}
            />
          </div>
        );
      })}
      <TextField
        label={path}
        value={newFieldName}
        onChange={(event) => setNewFieldName(event.target.value)}
        fullWidth
        margin="normal"
      />
      <Button onClick={handleAddField} variant="contained" color="primary" className="mb-2">
        Add Field
      </Button>
    </div>
  );
};

export default withJsonFormsControlProps(PatternPropertiesRenderer);

I also have issues with the final form data generated but I’m sure I can resolve that after I’ve had some guidance. My understanding of JSON Forms is still limited so forgive me if my solution so far is completely wrong, just looking for some direction if there is a better way to resolve my issues.

Thanks in advance!

Hi @Moo

I think the issue is that you take the uischema prop that your custom renderer gets and directly pass it down to the JsonForms component in your renderer.
This works fine as long as you did not add any custom properties, yet. However, after you add them, you need to add them to the UISchema that the nested JsonForms gets. This does not happen automatically by adding the property to the data (as you do in handleFieldChange).

Best regards,
Lucas

Hi Lucas,

Thank you for the reply.

I understand now that I need to update the UI Schema but could you shed any light on whether my approach is correct? The JSON Schemas I’m using contain multiple nested patternProperties so I thought recursively calling a custom renderer could work but is causing issues with the generated form data.

Do you have any suggestions on how I should approach the issue?

Should the custom renderer only be called to aid in the creation of a key that fits the patternProperties along with the addition of that key to the UISchema?

I recall in another post there was a mention of a form/schema/path tuple but I couldn’t quite wrap my head around it.

Thank you

Hi @Moo,

From a UX standpoint your approach is good, I would also show a way to add/remove the additional properties. Delegating back to JSON Forms also makes sense.

However the implementation should be done a bit differently:

  • Within JSON Forms we are not invoking the JsonForms component again as it is too heavy for that (e.g. it manages its own state and validation etc.). Instead we use JsonFormsDispatch. Please have a look at our existing renderers on how to use it.
  • Registering against the scope #/patternProperties is weird. Your renderer should likely be an object renderer which takes over renderering in case the object has patternProperties.
  • The custom object renderer then can reuse the existing MaterialObjectRenderer for the “normal” properties. I have linked the renderer above. See here for a tutorial on how to reuse renderers. Alternatively, as the renderer is very small anyway, you can just copy it as a starting point
  • Then I would recommend implementing an “AdditionalRenderers” or “PatternProperties” renderer which is used in your custom object renderer
  • This renderer then takes over to
    • show an “add” button with a text-input field to add new properties. Once for every “patternProperties” declararion in the schema (and “additionalProperties” as it’s basically the same feature), and
    • to analyze the current data object for previously added properties. For these properties it can then generate a fitting schema and uischema based on the “patternProperties” schema in the JSON Schema and then dispatch back to JSON Forms

Here is an example implementation for the Vue Vuetify renderer set and here it is used. Obviously it looks a bit different than React, however the concepts are all the same.

Hi @sdirix,

Thank you for your assistance so far.

I have attempted to replicate the vuetify solution in react and have made progress but still have a lot of issues. The primary issue I want to discuss first is adding enums that are defined in patternProperties.

I’ll share my patternProperties/additionalProperties solution so far, perhaps you could take a look and share your thoughts on whether it’s implemented correctly. I’ll omit anything specific to my app but will leave relevant stuff for JSON Forms.

Heres the function I call JSON Forms from initially:

//imports

const ShowForm = ({ dataJSON }) => {
//useStates/useEffects/APIcalls etc...

  const renderers = [
    {
      tester: ObjectRendererTester,
      renderer: ObjectRenderer
    },
    ...materialRenderers
  ];

  return (
    <Card elevation={0} variant="outlined">
      <CardHeader title={`JSON Form - ${dataJSON.name}`} />
      <CardContent>
        <JsonForms
          schema={dataSchema}
          uischema={uiSchema}
          data={formJSON}
          renderers={renderers}
          cells={materialCells}
          onChange={({ data }) => setJSON(data)}
          validationMode={validation}
        />
          <Button onClick={handleSave} className="mt-3 mb-2">
            Submit
          </Button>
      </CardContent>
    </Card>
  );
};

export default ShowForm;

Heres my tester:

import { rankWith, isObjectControl } from '@jsonforms/core';

export default rankWith(3, isObjectControl);

ObjectRenderer:

import React from 'react';
import { findUISchema, Generate } from '@jsonforms/core';
import { withJsonFormsControlProps, JsonFormsDispatch } from '@jsonforms/react';
import cloneDeep from 'lodash/cloneDeep';
import isEmpty from 'lodash/isEmpty';
import AdditionalProperties from './AdditionalProperties';

const ObjectRenderer = (props) => {
  const { visible, enabled, uischemas, schema, uischema, path, rootSchema } = props;
  const hasAdditionalProperties =
    !isEmpty(schema.patternProperties) || typeof schema.additionalProperties === 'object';

  const detailUiSchema = () => {
    const uiSchemaGenerator = () => {
      const uiSchema = Generate.uiSchema(schema, 'Group');
      if (isEmpty(path)) {
        uiSchema.type = 'VerticalLayout';
      } else {
        uiSchema.label = uischema.label;
      }
      return uiSchema;
    };

    let result = findUISchema(
      uischemas,
      schema,
      uischema.scope,
      path,
      uiSchemaGenerator,
      uischema,
      rootSchema
    );

    if (props.nested && props.nested.level > 0) {
      result = cloneDeep(result);
      result.options = {
        ...result.options,
        bare: true,
        alignLeft: props.nested.level >= 4 || props.nested.parentElement === 'array'
      };
    }

    return result;
  };

  return (
    <div>
      <JsonFormsDispatch
        visible={visible}
        enabled={enabled}
        schema={schema}
        uischema={detailUiSchema()}
        path={path}
      />
      {hasAdditionalProperties && <AdditionalProperties props={props} />}
    </div>
  );
};

export default withJsonFormsControlProps(ObjectRenderer);

AdditionalProperties:

import React, { useState, useEffect, useMemo } from 'react';
import { composePaths, createControlElement, createDefaultValue, Generate } from '@jsonforms/core';
import { JsonFormsDispatch } from '@jsonforms/react';
import Ajv from 'ajv';
import {
  TextField,
  IconButton,
  Tooltip,
  Card,
  CardContent,
  CardHeader,
  Grid,
  Box
} from '@mui/material';
import { Add as AddIcon, Delete as DeleteIcon } from '@mui/icons-material';

const reuseAjvForSchema = (ajv, schema) => {
  if (schema && (schema.id || schema.$id)) {
    ajv.removeSchema(schema);
  }
  return ajv;
};

const AdditionalProperties = ({ props }) => {
  const [newPropertyName, setNewPropertyName] = useState('');
  const [additionalPropertyItems, setAdditionalPropertyItems] = useState([]);

  const ajv = useMemo(() => new Ajv(), []);

  const reservedPropertyNames = useMemo(() => {
    return Object.keys((props.schema && props.schema.properties) || {});
  }, [props.schema]);

  const additionalKeys = useMemo(() => {
    return props.data && typeof props.data === 'object'
      ? Object.keys(props.data).filter((key) => !reservedPropertyNames.includes(key))
      : [];
  }, [props.data, reservedPropertyNames]);

  useEffect(() => {
    const items = additionalKeys.map((propName) =>
      toAdditionalPropertyType(propName, props.data[propName])
    );
    setAdditionalPropertyItems(items);
  }, [additionalKeys]);

  const toAdditionalPropertyType = (propName, propValue) => {
    let propSchema = undefined;
    let propUiSchema = undefined;

    if (props.schema?.patternProperties && typeof props.schema.patternProperties === 'object') {
      const matchedPattern = Object.keys(props.schema.patternProperties).find((pattern) =>
        new RegExp(pattern).test(propName)
      );
      if (matchedPattern) {
        propSchema = props.schema.patternProperties[matchedPattern];
      }
    }

    if (
      !propSchema &&
      props.schema?.additionalProperties &&
      typeof props.schema.additionalProperties === 'object'
    ) {
      propSchema = props.schema.additionalProperties;
    }

    if (!propSchema && propValue !== undefined) {
      propSchema = Generate.jsonSchema({ prop: propValue }).properties?.prop;
    }

    if (propSchema) {
      if (propSchema.type === 'object' || propSchema.type === 'array') {
        propUiSchema = Generate.uiSchema(propSchema, 'Group');
      } else {
        propUiSchema = createControlElement(composePaths(props.path, propName));
      }
    }

    return {
      propertyName: propName,
      path: composePaths(props.path, propName),
      schema: propSchema,
      uischema: propUiSchema
    };
  };

  const propertyNameValidator = useMemo(() => {
    let schema = undefined;

    if (props.schema?.patternProperties && typeof props.schema.patternProperties === 'object') {
      const patternKeys = Object.keys(props.schema.patternProperties).join('|');
      schema = { type: 'string', pattern: patternKeys };
    }

    return schema ? reuseAjvForSchema(ajv, schema).compile(schema) : undefined;
  }, [props.schema, ajv]);

  const handleAddProperty = () => {
    if (newPropertyName) {
      const newProperty = toAdditionalPropertyType(newPropertyName, undefined);
      setAdditionalPropertyItems([...additionalPropertyItems, newProperty]);

      if (props.data && typeof props.data === 'object') {
        props.data[newPropertyName] = createDefaultValue(newProperty.schema);
        props.handleChange(props.path, props.data);
      }
      setNewPropertyName('');
    }
  };

  const handleRemoveProperty = (propertyName) => {
    setAdditionalPropertyItems(
      additionalPropertyItems.filter((item) => item.propertyName !== propertyName)
    );

    if (props.data && typeof props.data === 'object') {
      delete props.data[propertyName];
      props.handleChange(props.path, props.data);
    }
  };

  const newPropertyErrors = () => {
    if (newPropertyName) {
      const messages = [];
      if (propertyNameValidator) {
        const valid = propertyNameValidator(newPropertyName);
        if (!valid) {
          messages.push(propertyNameValidator.errors.map((error) => error.message).join(', '));
        }
      }

      if (reservedPropertyNames.includes(newPropertyName)) {
        messages.push(`Property '${newPropertyName}' is already defined`);
      }

      if (additionalPropertyItems.find((ap) => ap.propertyName === newPropertyName)) {
        messages.push(`Property '${newPropertyName}' is already defined`);
      }

      return messages;
    }
    return [];
  };

  const addPropertyDisabled = useMemo(() => {
    const errors = newPropertyErrors();
    return (
      !props.enabled ||
      (props.schema?.maxProperties !== undefined &&
        Object.keys(props.data).length >= props.schema.maxProperties) ||
      errors.length > 0 ||
      !newPropertyName
    );
  }, [props, newPropertyName]);

  return (
    <Card elevation={0}>
      <CardHeader
        title={props.schema.title || 'Additional/Pattern Properties'}
        action={
          <Tooltip title="Add Property">
            <span>
              <IconButton
                aria-label="add"
                onClick={handleAddProperty}
                disabled={addPropertyDisabled}>
                <AddIcon />
              </IconButton>
            </span>
          </Tooltip>
        }
      />
      <CardContent>
        <TextField
          label="New Property"
          value={newPropertyName}
          onChange={(event) => setNewPropertyName(event.target.value)}
          error={newPropertyErrors().length > 0}
          helperText={newPropertyErrors().join(', ')}
          disabled={!props.enabled}
          sx={{ mb: 2 }}
        />

        <Grid container spacing={2}>
          {additionalPropertyItems.map((element, index) => (
            <Grid
              item
              xs={12}
              key={index}
              sx={{
                boxShadow: 2,
                borderRadius: 1,
                p: 2,
                minWidth: 300,
                marginTop: 2
              }}>
              <Box display="flex" alignItems="center" justifyContent="space-between">
                <CardHeader title={element.propertyName} sx={{ flexGrow: 1, paddingBottom: 2 }} />
                <Tooltip title="Delete Property">
                  <IconButton
                    aria-label="delete"
                    onClick={() => handleRemoveProperty(element.propertyName)}
                    disabled={!props.enabled}>
                    <DeleteIcon />
                  </IconButton>
                </Tooltip>
              </Box>
              <JsonFormsDispatch
                schema={element.schema}
                uischema={element.uischema}
                path={element.path}
                enabled={props.enabled}
              />
            </Grid>
          ))}
        </Grid>
      </CardContent>
    </Card>
  );
};

export default AdditionalProperties;

So with the above, I get a similar outcome to that presented in the Vuetify solution with the default schema/uischema/form etc.

When using an enum in a schema such as:

//rest of schema
"address": {
      "type": "object",
      "properties": {
        "street": {
          "type": "string",
          "enum": ["Main St", "Second St", "Third St"]
        },
        "city": {
          "type": "string",
          "enum": ["New York", "Los Angeles", "Chicago"]
        },
        "postalCode": {
          "type": "string",
          "enum": ["10001", "90001", "60601"]
        }
      },
      "required": ["street", "city"]
    }
//rest of schema

Everything works well, the enums are populated. I can trace that MuiAutocomplete line 85 is hit:

  const findOption = options.find((o) => o.value === data) ?? null;

Options is defined with something like:
{
label: “Main St”,
value: “Main St”,
}
for each of the items. (though ‘data’ is undefined, I’ll come back to this)

However, when attempting to use enums that have been selected from patternProperties such as:

//rest of schema
"patternProperties": {
    "^enum$": {
      "enum": [
        "foo",
        "bar",
        "foobar"
      ],
      "type": "string"
    },
//rest of schema

I experience the error:

ERROR
Cannot read properties of undefined (reading 'find')
TypeError: Cannot read properties of undefined (reading 'find')
    at MuiAutocomplete (http://localhost:3001/dbapp/static/js/bundle.js:20460:30)
    at renderWithHooks (http://localhost:3001/dbapp/static/js/bundle.js:140482:22)
    at mountIndeterminateComponent (http://localhost:3001/dbapp/static/js/bundle.js:143766:17)
    at beginWork (http://localhost:3001/dbapp/static/js/bundle.js:145062:20)
    at HTMLUnknownElement.callCallback (http://localhost:3001/dbapp/static/js/bundle.js:130078:18)
    at Object.invokeGuardedCallbackDev (http://localhost:3001/dbapp/static/js/bundle.js:130122:20)
    at invokeGuardedCallback (http://localhost:3001/dbapp/static/js/bundle.js:130179:35)
    at beginWork$1 (http://localhost:3001/dbapp/static/js/bundle.js:150043:11)
    at performUnitOfWork (http://localhost:3001/dbapp/static/js/bundle.js:149291:16)
    at workLoopSync (http://localhost:3001/dbapp/static/js/bundle.js:149214:9)

So when it hits MuiAutocomplete line 85 ‘options’ is undefined (as is data).

I attempted to troubleshoot and found this function:

export const createDefaultValue = (
  schema: JsonSchema,
  rootSchema: JsonSchema
) => {
  const resolvedSchema = Resolve.schema(schema, schema.$ref, rootSchema);
  if (resolvedSchema.default !== undefined) {
    return extractDefaults(resolvedSchema, rootSchema);
  }
  if (hasType(resolvedSchema, 'string')) {
    if (
      resolvedSchema.format === 'date-time' ||
      resolvedSchema.format === 'date' ||
      resolvedSchema.format === 'time'
    ) {
      return convertDateToString(new Date(), resolvedSchema.format);
    }
    return '';
  } else if (
    hasType(resolvedSchema, 'integer') ||
    hasType(resolvedSchema, 'number')
  ) {
    return 0;
  } else if (hasType(resolvedSchema, 'boolean')) {
    return false;
  } else if (hasType(resolvedSchema, 'array')) {
    return [];
  } else if (hasType(resolvedSchema, 'object')) {
    return extractDefaults(resolvedSchema, rootSchema);
  } else if (hasType(resolvedSchema, 'null')) {
    return null;
  } else {
    return {};
  }
};

Doesn’t seem to handle enums. I tried this:

export const createDefaultValue = (
  schema: JsonSchema,
  rootSchema: JsonSchema
) => {
  const resolvedSchema = Resolve.schema(schema, schema.$ref, rootSchema);

  // Check if a default value is specified in the schema
  if (resolvedSchema.default !== undefined) {
    return extractDefaults(resolvedSchema, rootSchema);
  }
  
  // Handle enum values
  if (Array.isArray(resolvedSchema.enum) && resolvedSchema.enum.length > 0) {
    return resolvedSchema.enum[0]; // Choose the first value in the enum list
  }

  if (hasType(resolvedSchema, 'string')) {
    if (
      resolvedSchema.format === 'date-time' ||
      resolvedSchema.format === 'date' ||
      resolvedSchema.format === 'time'
    ) {
      return convertDateToString(new Date(), resolvedSchema.format);
    }
    return '';
  } else if (
    hasType(resolvedSchema, 'integer') ||
    hasType(resolvedSchema, 'number')
  ) {
    return 0;
  } else if (hasType(resolvedSchema, 'boolean')) {
    return false;
  } else if (hasType(resolvedSchema, 'array')) {
    return [];
  } else if (hasType(resolvedSchema, 'object')) {
    return extractDefaults(resolvedSchema, rootSchema);
  } else if (hasType(resolvedSchema, 'null')) {
    return null;
  } else {
    return {};
  }
};

And found that when I reach line 85, data was now = ‘foo’ but options is still, of course, undefined.

I can quite figure out how to trace where options is defined to ensure it is populated and doesn’t throw an error, appreciate any advice.

Heres a json schema you can test in Vuetify and see the same issue (note enums out of patternProperties are fine but ones added through patternProperties are not):

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "status": {
      "type": "string",
      "enum": ["pending", "approved", "rejected"]
    },
    "priority": {
      "type": "string",
      "enum": ["low", "medium", "high"]
    },
    "category": {
      "type": "string",
      "enum": ["bug", "feature", "task"]
    },
    "details": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        },
        "age": {
          "type": "integer",
          "minimum": 0,
          "enum": [18, 25, 30, 40]
        },
        "gender": {
          "type": "string",
          "enum": ["male", "female", "other"]
        }
      },
      "required": ["name", "age", "gender"]
    },
    "additionalInfo": {
      "type": "object",
      "properties": {
        "description": {
          "type": "string"
        },
        "type": {
          "type": "string",
          "enum": ["note", "warning", "info"]
        }
      },
      "patternProperties": {
        "^info[0-9]+$": {
          "type": "string",
          "enum": ["info1", "info2", "info3"]
        }
      }
    }
  },
  "definitions": {
    "address": {
      "type": "object",
      "properties": {
        "street": {
          "type": "string",
          "enum": ["Main St", "Second St", "Third St"]
        },
        "city": {
          "type": "string",
          "enum": ["New York", "Los Angeles", "Chicago"]
        },
        "postalCode": {
          "type": "string",
          "enum": ["10001", "90001", "60601"]
        }
      },
      "required": ["street", "city"]
    },
    "contact": {
      "type": "object",
      "properties": {
        "email": {
          "type": "string",
          "format": "email"
        },
        "phone": {
          "type": "string",
          "pattern": "^\\+?[1-9]\\d{1,14}$"
        }
      },
      "required": ["email"]
    }
  },
  "patternProperties": {
    "^customProperty[0-9]+$": {
      "$ref": "#/definitions/address"
    },
    "^contact[0-9]+$": {
      "$ref": "#/definitions/contact"
    }
  },
  "additionalProperties": false
}

Thanks in advance!

Moo

Hi @Moo,

Thanks for the extensive example.

  • Extending the default value mechanism to also check for enums makes sense I think.
  • I tested the schema you posted in Vuetify and there it seems to work for me. Note that the example uses very specific regex patterns, so it’s easy to enter a name which does not match to any of them. That it doesn’t work then is fine I think.

AdditionalPropertiesVuetify

You can see in the GIF above that adding a customPropertyX and contactY works, but adding another property which does not match will fail.

I did not yet manage to look at your code. It would be helpful if you could provide it as a StackBlitz or as a repository, as setting it up on my side takes some time.

Hi @sdirix,

Thanks for your response.

I can see from your screen recording that the problematic adding of enums from patternProperties hasn’t been tested, perhaps I was unclear, my apologies. I have cut out all other enums properties and left just the problematic one so you can test:

{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "type": "object",
    "patternProperties": {
        "^info[0-9]+$": {
            "type": "string",
            "enum": [
                "info1",
                "info2",
                "info3"
            ]
        }
    },
    "additionalProperties": false
}

So if you try to add, for example, “info1”, I get this in Vuetify (and an error in my project):


I have created a codesandbox environment for you to test my code. The JSON schema includes just the content I’m having issues with, but I’ve included the larger schema for you to test also.

Let me know if you’re able to replicate the issue.

Thanks

Hi @Moo,

Thank you for the detailed explanation and the codesandbox :heart:. I’m now able to reproduce the enum issue in Vuetify as well as the runtime error you encounter.

I had a look at the code in codesandbox. I checked the output of toAdditionalPropertyType. An example output looked like this:

{
    "propertyName": "info78",
    "path": "info78",
    "schema": {
        "type": "string",
        "enum": [
            "info1",
            "info2",
            "info3"
        ]
    },
    "uischema": {
        "type": "Control",
        "scope": "info78"
    }
}

The uischema and schema don’t fit together and therefore you run into problems down the road. As the schema is directly a property for which we want a Control to show, the uischema should look like this

{
    "type": "Control",
    "scope": "#"
}

This uischema declares to render a Control for the root of the associated schema.