React JSON Schema Form

vudodov

Valerii Udodov

Posted on May 6, 2021

React JSON Schema Form

Today, I'd like to share with you one of the items from my tools-belt, which I'm successfully using for years now. It is simply a react component. It is a form. But not just a form, it is a form that allows anyone independently of their React or HTML knowledge to build a sophisticated feature-rich form based on any arbitrary expected data in a consistent manner.

Behold, the React JSON Schema Form, or simply RJSF. Originally started and built as an Open Source project by the Mozilla team. Evolved into a separate independent project.

Out of the box, RJSF provides us with rich customization of different form levels, extensibility, and data validation. We will talk about each aspect separately.

Configuration

JSON Schema

The end goal of any web form is to capture expected user input. The RJSF will capture the data as a JSON object. Before capturing expected data we need to define how the data will look like. The rest RJSF will do for us. To define and annotate the data we will use another JSON object. Bear with me here...
We will be defining the shape (or the schema) of the JSON object (the data) with another JSON object. The JSON object that defines the schema for another JSON object is called -drumroll- JSON Schema and follows the convention described in the JSON Schema standard.

To make things clear, we have two JSON objects so far. One representing the data we are interested in, another representing the schema of the data we are interested in. The last one will help RJSF to decide which input to set for each data attribute.


A while ago in one of my previous articles I've touched base on the JSON Schema.

I'm not going to repeat myself, I'll just distill to what I think is the most valuable aspect of it.
JSON Schema allows us to capture changing data and keep it meaningful. Think of arbitrary address data in the international application. Address differs from country to country, but the ultimate value doesn't. It represents a point in the world that is described with different notations. Hence even though the address format in the USA, Spain, Australia, or China is absolutely different, the ultimate value -from an application perspective- is the same-- a point on the Globe. It well might be employee home address, parcel destination or anything else and notation does not change this fact.


So if we want to capture, let's say, the first and last name and telephone number of a person. The expected data JSON object will look like

{
  "firstName": "Chuck",
  "lastName": "Norris",
  "telephone": "123 456 789"
}
Enter fullscreen mode Exit fullscreen mode

And the JSON Schema object to define the shape of the data object above will look like

{
  "title": "A person information",
  "description": "A simple person data.",
  "type": "object",
  "properties": {
    "firstName": {
      "type": "string",
      "title": "First name",
    },
    "lastName": {
      "type": "string",
      "title": "Last name"
    },
    "telephone": {
      "type": "string",
      "title": "Telephone",
      "minLength": 10
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Something to keep in mind.
JSON Schema is following a permissive model. Meaning out of the box everything is allowed. The more details you specify, the more restrictions you put in place. So it is worth sometimes religiously define the expected data.


This is the bare minimum we need to start. Let's look at how the JSON Schema from the above will look like as a form. Just before let's also look at the code...

import Form from "@rjsf/core";

// ...

    <Form schema={schema}>
      <div />
    </Form>

// ...
Enter fullscreen mode Exit fullscreen mode

Yup, that's it, now let's check out the form itself

Alt Text

UI Schema

Out of the box, the RJSF makes a judgment on how to render one field or another. Using JSON Schema you primarily control what to render, but using UI Schema you can control how to render.

UI Schema is yet another JSON that follows the tree structure of the JSON data, hence form. It has quite some stuff out of the box.

You can be as granular as picking a color for a particular input or as generic as defining a template for all fields for a string type.

Let's try to do something with our demo form and say disable the first name and add help text for the phone number.

{
    "firstName": {
        "ui:disabled": true
    },
    "telephone": {
        "ui:help": "The phone number that can be used to contact you"
    }
}

Enter fullscreen mode Exit fullscreen mode

Let's tweak our component a bit

import Form from "@rjsf/core";

// ...

    <Form 
        schema={schema}
        uiSchema={uiSchema}
    >
      <div />
    </Form>

// ...
Enter fullscreen mode Exit fullscreen mode

And here is the final look

Alt Text

Nice and easy. There's a lot of built-in configurations that are ready to be used, but if nothing suits your needs, you can build your own...

Customization

The API allows to specify your own custom widget and field components:

  • A widget represents a HTML tag for the user to enter data, eg. input, select, etc.
  • A field usually wraps one or more widgets and most often handles internal field state; think of a field as a form row, including the labels.

-- RJSF Documentation

Another way to think of it is field includes label and other stuff around, while widget only the interaction component or simply input.

For the sake of example let's create a simple text widget that will make the input red and put a dash sign (-) after every character.

To keep things light and simple let's imagine that the whole form will be a single red field. The JSON Schema will look as follows

const schema = {
  title: "Mad Field",
  type: "string"
};
Enter fullscreen mode Exit fullscreen mode

Forgot to say that widgets are just components, that will be mounted in and will receive a standard set of props. No limits, just your imagination ;)

const MadTextWidget = (props) => {
  return (
    <input type="text"
      style={{backgroundColor: "red"}}
      className="custom"
      value={props.value}
      required={props.required}
      onChange={(event) => props.onChange(event.target.value + " - ")} />
  );
};
Enter fullscreen mode Exit fullscreen mode

The next step is to register the widget so that we can use it in the UI Schema

const widgets = {
  madTextWidget: MadTextWidget
}
Enter fullscreen mode Exit fullscreen mode

Finally, we can define the UI Schema

const uiSchema = {
  "ui:widget": "madTextWidget"
};
Enter fullscreen mode Exit fullscreen mode

And the full code with the RJSF

const schema = {
  title: "Mad Field",
  type: "string"
};

const MadTextWidget = (props) => {
  return (
    <input type="text"
      style={{backgroundColor: "red"}}
      className="custom"
      value={props.value}
      required={props.required}
      onChange={(event) => props.onChange(event.target.value + " - ")} />
  );
};

const widgets = {
  madTextWidget: MadTextWidget
}

const uiSchema = {
  "ui:widget": "madTextWidget"
};

ReactDOM.render((
  <Form schema={schema}
        uiSchema={uiSchema} 
        widgets={widgets}
    />
), document.getElementById("app"));
Enter fullscreen mode Exit fullscreen mode

It will look like this

Alt Text

Here, try it yourself. The field will be pretty similar but will have a wider impact area so to speak. As been said the field will include labels and everything around the input itself.

Custom templates allows you to re-define the layout for certain data types (simple field, array or object) on the form level.

Finally, you can build your own Theme which will contain all your custom widgets, fields, template other properties available for a Form component.

Validation

As was mentioned before the JSON Schema defines the shape of the JSON data that we hope to capture with the form. JSON Schema allows us to define the shape fairly precisely. We can tune the definition beyond the expected type, e.g. we can define a length of the string or an email regexp or a top boundary for a numeric value and so forth.

Check out this example

const Form = JSONSchemaForm.default;
const schema = {
  type: "string",
  minLength: 5
};

const formData = "Hi";

ReactDOM.render((
  <Form schema={schema} formData={formData} liveValidate />
), document.getElementById("app"));
Enter fullscreen mode Exit fullscreen mode

Will end up looking like this

Alt Text

Of course, we can re-define messages, configure when, where, and how to show the error messages.

Out of the box our data will be validated against the JSON Schema using the (Ajv) A JSON Schema validator library. However, if we want to, we can implement our own custom validation process.

Dependencies

Dependencies allow us to add some action to the form. We can dynamically change form depending on the user input. Basically, we can request extra information depending on what the user enters.


Before we will get into dependencies, we need to get ourselves familiar with dynamic schema permutation. Don't worry, it is easier than it sounds. We just need to know what four key-words mean

  • allOf: Must be valid against all of the subschemas
  • anyOf: Must be valid against any of the subschemas
  • oneOf: Must be valid against exactly one of the subschemas
  • not: Must not be valid against the given schema ___

Although dependencies have been removed in the latest JSON Schema standard versions, RJSF still supports it. Hence you can use it, there are no plans for it to be removed so far.

Property dependencies

We may define that if one piece of the data has been filled, the other piece becomes mandatory. There are two ways to define this sort of relationship: unidirectional and bidirectional. Unidirectional as you might guess from the name will work in one direction. Bidirectional will work in both, so no matter which piece of data you fill in, the other will be required as well.

Let's try to use bidirectional dependency to define address in the shape of coordinates. The dependency will state that if one of the coordinates has been filled, the other one has to be filled in either. But if none is filled, none is required.

{
  "type": "object",
  "title": "Longitude and Latitude Values",
  "description": "A geographical coordinate.",
  "properties": {
    "latitude": {
      "type": "number",
      "minimum": -90,
      "maximum": 90
     },
    "longitude": {
      "type": "number",
      "minimum": -180,
      "maximum": 180
    }
  },
  "dependencies": {
    "latitude": [
      "longitude"
    ],
    "longitude": [
      "latitude"
    ]
  },
  "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode

See lines 17 to 24. That's all there is to it, really. Once we will pass this schema to the form, we will see the following (watch for an asterisk (*) near the label, it indicates whether the field is mandatory or not).

Alt Text

Schema dependencies

This one is more entertaining, we can actually control visibility through the dependencies. Let's follow up on the previous example and for the sake of the example show longitude only if latitude is filled in.

{
  "type": "object",
  "title": "Longitude and Latitude Values",
  "description": "A geographical coordinate.",
  "properties": {
    "latitude": {
      "type": "number",
      "minimum": -90,
      "maximum": 90
     }
  },
  "dependencies": {
    "latitude": {
      "properties": {
        "longitude": {
          "type": "number",
          "minimum": -180,
          "maximum": 180
          }
      }
    }
  },
  "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode

No code changes are required, just a small dependency configuration tweak (lines 12 to 22).

Alt Text

Dynamic schema dependencies

So far so good, pretty straightforward. We input the data, we change the expected data requirements. But we can go one step further and have multiple requirements. Not only based on whether the data is presented or not but on the value of presented data.

Alt Text

Once again, no code, only JSON Schema modification

{
  "title": "How many inputs do you need?",
  "type": "object",
  "properties": {
    "How many inputs do you need?": {
      "type": "string",
      "enum": [
        "None",
        "One",
        "Two"
      ],
      "default": "None"
    }
  },
  "required": [
    "How many inputs do you need?"
  ],
  "dependencies": {
    "How many inputs do you need?": {
      "oneOf": [
        {
          "properties": {
            "How many inputs do you need?": {
              "enum": [
                "None"
              ]
            }
          }
        },
        {
          "properties": {
            "How many inputs do you need?": {
              "enum": [
                "One"
              ]
            },
            "First input": {
              "type": "number"
            }
          }
        },
        {
          "properties": {
            "How many inputs do you need?": {
              "enum": [
                "Two"
              ]
            },
            "First input": {
              "type": "number"
            },
            "Second input": {
              "type": "number"
            }
          }
        }
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Bottom line

Even though we went through some major concepts and features, we are far away from covering everything that RJSF empowers us to do.

I'd encourage you to check out official documentation for more insights and examples, GitHub repository for undocumented goodies and live playground to get your hands dirty. Finally, worth mentioning that the Open Source community keeps things going, so look outside these resources, there are quite a few good things over there.

RJSF is a ridiculously powerful thing if you need to customize and capture meaningful data. Enjoy!

💖 💪 🙅 🚩
vudodov
Valerii Udodov

Posted on May 6, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

React JSON Schema Form
react React JSON Schema Form

May 6, 2021