Support for patternProperties?

I’ve started playing around with jsonforms and it doesn’t appear that patternProperties are supported. Is that the case? Any plans to support this soon?

Hi @dieseldjango, this is a pretty advanced use case. We don’t support it at the moment and it’s not on our priority list for now, however we’ll definitely consider a good contribution.

Using a custom renderer you can definitely add patternProperties support to your forms. Let me know in case you need help with that.

Thanks for the quick response @sdirix. I’m new to your package, so any pointers on how to create a custom renderer would be appreciated.

I would recommend checking out the tutorial. Also all already provided renderers use the very same mechanism as a custom renderer, so you can also look through the JSON Forms code base when you are wondering how a specific renderer is implemented.

I am looking to create a custom renderer for a JSON object that looks like

{
  "rules": {
     "<NameHere>": {
        ...
     },
     "<NameHere>": {
        ...
     }
  ....
}

What I want is some kind ability to add/delete a new field/property. I am imagining something similar to what I see with arrays with a [+] button and a field name to be entered. The schema of the fields is structured so I want to pass control back to Json Forms to render the body of the property. What I’m looking to get a feel for is the high level “algorithm” or key building blocks that I should be looking at within my custom renderer.

Hi @kolban-google,

for this use case you need to solve two challenges:

Rendering a UI for existing ‘dynamic’ properties. One way of implementing the functionality is with a custom renderer for the element which contains the patternProperties. In the custom renderer you determine all “dynamic” properties, and for each one generate a schema & uischema & path tupel. You can then hand these over to the dispatch mechanism of JSON Forms to render the UI for your property.

Adding new properties. With the logic above already in place, all you need to do is to add a new attribute into the data. For this you can use the normal handleChange mechanism we have.

Does this make sense to you? There could also be other approaches (e.g. modifying the overall JSON Schema to also represent the dynamic data) but I think the suggested one is the most flexible for now.

Thanks for the response. I’m studying hard and get the “gist” of your response. I’ve read the tutorial over and over but it’s not sticking. I think what I need to find are some guides for building custom renderers that are richer. For example, in your last response I read I need to generate a schema & uischema. This has me lost as I provided a ull JSON schema and JsonForms UI Schema at the start. You mentioned path … that too has me so far lost as I don’t yet understand that in context. You mentioned dispatching to JSON Forms … again that leaves me confused. It’s like a problem at my end in comprehension … so I’ll keep on trying. If you know of any additional guides to writing custom renderers, let me know.

Neil

Later …

I created a schema that looks like:

{
  "type": "object",
  "properties": {
    "prop1": {
      "type": "object",
      "patternProperties": {
        "^.*$": {
          "type": "object",
          "properties": {
            "X": {
              "type": "string"
            },
            "Y": {
              "type": "string"
            }
          }
        }
      }
    }
  }
};

This should match data of the form:

{
  "prop1": {
    "myname1": { "X": "X1", "Y": "Y1"},
    "myname2": { "X": "X2", "Y": "Y2"},
  }
}

My UI Schema was:

{
  "type": "VerticalLayout",
  "elements": [
    {
      "type": "Control",
      "label": true,
      "scope": "#/properties/prop1"
    }
  ] 
}

I then wrote a custom renderer where the “Tester” always returned false. What I saw was that it was called with:

  • uischema: type = VerticalLayout
  • uischema: type = Control, scope = #/properties/prop1

For this one, schema was:

{
  "type": "object",
  "properties": {
    "prop1": {
      "type": "object",
      "patternProperties": {
        "^.*$": {
          "type": "object",
          "properties": {
            "X": {
              "type": "string"
            },
            "Y": {
              "type": "string"
            }
          }
        }
      }
    }
  }
}
  • uischema: type = Group, elements = []

For this one, schema was:

{
  "type": "object",
  "patternProperties": {
    "^.*$": {
      "type": "object",
      "properties": {
        "X": {
          "type": "string"
        },
        "Y": {
          "type": "string"
        }
      }
    }
  }
}

Based on all this, what might I choose to code my tester to match upon?

Still studying on this … pulling apart the hints.

One way of implementing the functionality is with a custom renderer for the element which contains the patternProperties .

Should I be looking at a layout renderer or a control renderer?
For the tester, can I just look for “patternProperties” in the schema or should I be looking to create some kind of custom “type” in the uischema…

In the custom renderer you determine all “dynamic” properties, and for each one generate a schema & uischema & path tupel. You can then hand these over to the dispatch mechanism of JSON Forms to render the UI for your property.

I suspect that part of the thinking on this one will be whether or not it is a layout renderer or a control renderer. For determining the dynamic properties, I’m thinking I’ll have to access the data …

Using my previous example, if my data is:

{
  "prop1": {
    "myname1": { "X": "X1", "Y": "Y1"},
    "myname2": { "X": "X2", "Y": "Y2"},
  }
}

Then my dynamic properties will be “myname1” and “myname2”. However I’m lost on what we mean by generate a schema, uischema and path. I’m also lost on the dispatch mechanism.

Hi @kolban-google,

JSON Forms is very flexible therefore there are many ways to implement the feature. The way you want to implement this mostly relies on your actual use cases.

Should I be looking at a layout renderer or a control renderer?

We don’t really differentiate the two. All testers are invoked on all UI Schema elements. Our off-the-shelf renderer sets come with separate renderers for our defined UI Schema elements and many renderers for type: 'Control', which is why we refer to them as control renderers. Whether you want to implement a “layout” or a “control” renderer is based on the way you want to support the “patternProperties” objects.

If you know of any additional guides to writing custom renderers, let me know.

There is no difference between our provided renderers and custom renderers. All of them use the exact same patterns and mechanism. Therefore you can just browse through our code base and see how we implemented the existing renderers as a guidance.

A generic fit-all solution would be to implement a custom renderer for objects containing patternProperties. So the tester for this renderer has to look at the given schema and check whether it’s of type object and has a patternProperties attribue. If that’s the case you can return a high priority to let your renderer “win”.

For the implementation I would look at my suggestions and check how other renderers (like the existing object renderer) are implemented.

1 Like

I created a tester with logic as follows:

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

function isAdditionalProperties(uischema, schema) {
  console.log(`CR1_tester called uischema:${JSON.stringify(uischema, null, 2)} and schema:${JSON.stringify(schema, null, 2)}`)
  if (schema.hasOwnProperty("type") && schema.type === "object" && schema.hasOwnProperty("patternProperties")) {
    console.log("TRUE")
    return true;
  }
  return false;
}
export default rankWith(
  3, //increase rank as needed
  isAdditionalProperties
);

This seems to evaluate to true at the correct place. My renderer logic (just for testing), looks like:

import * as React from 'react';
import { withJsonFormsDetailProps } from '@jsonforms/react';
import TextField from '@mui/material/TextField';

const LR1 = (data) => {
    let { uischema, schema} = data;
    console.log(`LR1 called uischema:${JSON.stringify(uischema, null, 2)}, schema:${JSON.stringify(schema, null, 2)}`)
    return (
        <div>
            <TextField id="outlined-basic" label="Outlined" variant="outlined" />
        </div>
    );
};

export default withJsonFormsDetailProps(LR1);

Unfortunately, when it runs, we get a runtime error in the chrome browser:

I had expected a call into the Renderer implementation.

Hi @kolban-google,

the issue here is that your predicate really only checks the schema. Therefore it also returns true for UI Schema elements which are not type: 'Control', in your case for the Group UI Schema element. The problem then arises because the withJsonFormsDetailProps binding expects to be only called on type: 'Control' elements which always contain a scope. This scope does not exist in the Group element and therefore the binding crashes.

I would recommend additionally checking the UI Schema for type: 'Control'. See for example this helper we offer which does check both the UI Schema and the JSON Schema.

My puzzle is that in my initial tester, I always returned “false” just to see what was passed to the testers. What I see for the schema, uischema and data used in my previous posts is:

Call #1

CR1_tester called uischema:{
  "type": "VerticalLayout",
  "elements": [
    {
      "type": "Control",
      "label": "XYZ",
      "scope": "#/properties/prop1",
      "options": {
        "isPatternProperties": true
      }
    }
  ]
} and schema:{
  "type": "object",
  "properties": {
    "prop1": {
      "type": "object",
      "patternProperties": {
        "^.*$": {
          "type": "object",
          "properties": {
            "X": {
              "type": "string"
            },
            "Y": {
              "type": "string"
            }
          }
        }
      }
    }
  }

Call #2

CR1_tester called uischema:{
  "type": "VerticalLayout",
  "elements": [
    {
      "type": "Control",
      "label": "XYZ",
      "scope": "#/properties/prop1",
      "options": {
        "isPatternProperties": true
      }
    }
  ]
} and schema:{
  "type": "object",
  "properties": {
    "prop1": {
      "type": "object",
      "patternProperties": {
        "^.*$": {
          "type": "object",
          "properties": {
            "X": {
              "type": "string"
            },
            "Y": {
              "type": "string"
            }
          }
        }
      }
    }
  }
}

Call #3

CR1_tester called uischema:{
  "type": "Group",
  "elements": [],
  "label": "XYZ"
} and schema:{
  "type": "object",
  "patternProperties": {
    "^.*$": {
      "type": "object",
      "properties": {
        "X": {
          "type": "string"
        },
        "Y": {
          "type": "string"
        }
      }
    }
  }
}

It is #3 that I am interpreting I should be catching … but I see it being passed through as a Group and not as a Control. This makes me think that the predicate you suggested in the last posting wouldn’t match?

Looking at your post I’m assuming that the original UI Schema you hand over to JSON Forms is


  "type": "VerticalLayout",
  "elements": [
    {
      "type": "Control",
      "label": "XYZ",
      "scope": "#/properties/prop1",
      "options": {
        "isPatternProperties": true
      }
    }
  ]
}

The flow that I expect is the following:

  1. A renderer is determined for the initial schema and UI schema. As the UI Schema is of type VerticalLayout, the MaterialVerticalLayout will win.

  2. The layout renderer just dispatches again to JSON Forms for all its children. Therefore the next time the tester is invoked, it should not be with the full UI Schema as indicated in your post but with the child, i.e. the UI schema should be the following

     {
       "type": "Control",
       "label": "XYZ",
       "scope": "#/properties/prop1",
       "options": {
         "isPatternProperties": true
       }
     }
    

    In this case the MaterialObjectRenderer will win by default. What this renderer does is to find or generate a detail UI schema for all its properties.
    In your case there are NO properties, therefore what will be generated is an empty layout, i.e. the type: 'Group' which you also saw.

  3. The group renderer is invoked on the group, which behaves very similar to the vertical layout, just with some slightly different visuals.

Therefore I would like to suggest to recheck what you actually received in “Call #2” for the UI Schema. I would have expected only the Control element.

This would then also have been the place where your predicate should return true. As I’m just noticing: You should use the schemaMatches helper within your tester to properly resolve the schema to the one which you are interested in before invoking your predicate on it. Again see the already existing isObjectControl helper as a reference which internally invokes schemaTypeIs which uses schemaMatches.

Showing in YAML … my uischema is:

type: VerticalLayout
elements:
- type: Control
  label: XYZ
  scope: "#/properties/prop1"

and my JSON Schema is:

type: object
properties:
  prop1:
    type: object
    patternProperties:
      "^.*$":
        type: object
        properties:
          X:
            type: string
          Y:
            type: string

and my data is:

{
  "prop1": {
    "myname1": { "X": "X1", "Y": "Y1"},
    "myname2": { "X": "X2", "Y": "Y2"},
  }
}

The log messages from my tester continue to be the same as before. I note the tester seems to be called “twice” for each entry so I simplify to one entry.

Call type 1

CR1_tester called with >>> uischema:
---
type: VerticalLayout
elements:
  - type: Control
    label: XYZ
    scope: '#/properties/prop1'

--- >>> schema:
---
type: object
properties:
  prop1:
    type: object
    patternProperties:
      ^.*$:
        type: object
        properties:
          X:
            type: string
          'Y':
            type: string

Call type 2

CR1_tester called with >>> uischema:
---
type: Control
label: XYZ
scope: '#/properties/prop1'

--- >>> schema:
---
type: object
properties:
  prop1:
    type: object
    patternProperties:
      ^.*$:
        type: object
        properties:
          X:
            type: string
          'Y':

Call type 3

CR1_tester called with >>> uischema:
---
type: Group
elements: []
label: XYZ

--- >>> schema:
---
type: object
patternProperties:
  ^.*$:
    type: object
    properties:
      X:
        type: string
      'Y':
        type: string

Are you suggesting that I return “true” and hence register to handle the tester call for “Call type 2” in this post?

Hi @kolban-google,

Are you suggesting that I return “true” and hence register to handle the tester call for “Call type 2” in this post?

Yes that’s it. The calls look exactly as I expected and described above. In “Call 2” JSON Forms is looking for a renderer for the Control which points to the type: 'object' which has the patternProperties. For this your custom renderer should take over to handle the patternProperties in the way you expect.

I note the tester seems to be called “twice” for each entry so I simplify to one entry.

Not sure why this happens. Are they called as 1,2,3,1,2,3 or as 1,1,2,2,3,3? Can you show how you actually invoke JSON Forms?

Here is my code of how I am invoking JsonForms

function App() {

  const schema_data = yaml.load(schema_data_yaml);

  const initialData = {
    "prop1": {
      "myname1": { "X": "X1", "Y": "Y1"},
      "myname2": { "X": "X2", "Y": "Y2"},
    }
  };
const uischema = yaml.load(uischema_yaml);
      
  const [data, setData] = useState(initialData);
  const [schema, setSchema] = useState(schema_data)


  return (
    <div>
      <JsonForms
        schema={schema}
        uischema={uischema}
        data={data}
        renderers={renderers}
        cells={materialCells}
      />
      <Button variant="contained">Test</Button>
    </div>
  );
}

export default App;

The duplicates are 1,1,2,2,3,3.

As always … thanks for your help … I am now understanding you to say that my custom renderer should capture this instance:

CR1_tester called with >>> uischema:
---
type: Control
label: XYZ
scope: '#/properties/prop1'

--- >>> schema:
---
type: object
properties:
  prop1:
    type: object
    patternProperties:
      ^.*$:
        type: object
        properties:
          X:
            type: string
          'Y':
            type: string

What is causing me pause now is what to do next? I get a sense that a general algorithm would be:

for each of the properties of the json schema of type object
  if the current property is itself of "type object" AND it has a property called "patternProperties" then
   Do something special
  else
   Dispatch back to JSON Forms for handling

What is causing me the most confusion is getting my mind around the architecture and data flows.

1 Like

I’m still having a devil of a time trying to get my mind around this. I think I’m suffering from “Cognitive Overload”. Today I even bought a book on that topic to see how I could change my way of thinking to learn how to reduce the cognitive load. Using that as a notion … it dawned on me “Stefan is saying to use MaterialObjectRenderer as a reference as the default implementation would invoke that”. I read through the code but I wasn’t groking anything obvious so I then used my Chrome debugger to set a break point in the code and see what it does by default. The screen shot is next:

as I looked at the content shown in the image I noted:

  1. I don’t understand what findUISchema is semantically supposed to do.
  2. I don’t see any data in the scope of the JavaScript function. Surely I will need data in order to determine what actual properties I have?
  3. Are we suggesting that when I call my own JsonFormsDispatch in my own custom renderer should the “schema” object I pass in have “patternProperties” replaced with a “properties” and a sub entry for each property detected in the data using the pattenProperties matches as a template? Same for uischema … should I try and populate matching uischema entries for each of the detected pattern properties?

What is causing me the most confusion is getting my mind around the architecture and data flows.

The JSON Forms main pattern is a tester-based system to invoke renderers which take over parts of rendering a form and which are able to delegate back to the framework.

  • @jsonforms/core consists solely out of util functions to manage a reducer-based state for JSON Schema based forms. Most important are the core state and the already provided util functions to map a combination of dispatching props and the state to a specific set of props for a renderer.
  • To make them conveniently usable we already provide integrations into application frameworks like React. The state is setup by the root JsonForms component by using React contexts. Then we offer convenient HOCs (e.g. withJsonFormsControlProps or withJsonFormsDetailProps) to consume the state and the dispatch props and map them to the props which the specific renderer wants.

That’s basically it. There is nothing special about any of it, i.e. a custom renderer can even use their own state mappers and their own bindings if it wants to. To explore the state you can use the useJsonForms() hook however note that this will break our default memoization.

  1. I don’t understand what findUISchema is semantically supposed to do.

The default object renderer takes over the rendering of an object. What we implemented is that it just “finds” a UI Schema for its properties and then delegates back to the framework to rendering that UI Schema. “find” is multi purpose here: It checks all foreseen possibilities for a user to define a UI Schema themselves, if there is none it will just generate a default UI Schema.

  1. I don’t see any data in the scope of the JavaScript function. Surely I will need data in order to determine what actual properties I have?

data is available, however as the concrete renderer instance here doesn’t access it it just does not deconstruct it from the props. To see what is available via the props please follow the withJsonFormsDetailProps, see what it does and try to understand the data flows here.

  1. Are we suggesting that when I call my own JsonFormsDispatch in my own custom renderer should the “schema” object I pass in have “patternProperties” replaced with a “properties” and a sub entry for each property detected in the data using the pattenProperties matches as a template? Same for uischema … should I try and populate matching uischema entries for each of the detected pattern properties?

It’s completely up to you what you want to do, for example you could just render inputs yourself here. But yes, I think the easiest way to go forward would be to generate your own schema and UI schema for all existing patternProperties and then invoking JsonFormsDispatch for it so that JSON Forms takes over the rendering.