This page looks best with JavaScript enabled

React Json Schema Form

 ·  ☕ 11 min read

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.

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

1
2
3
4
5
{
  "firstName": "Chuck",
  "lastName": "Norris",
  "telephone": "123 456 789"
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{
  "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
    }
  }
}

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…

1
2
3
4
5
6
7
8
9
import Form from "@rjsf/core";

// ...

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

// ...

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

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.

1
2
3
4
5
6
7
8
{
  "firstName": {
    "ui:disabled": true
  },
  "telephone": {
    "ui:help": "The phone number that can be used to contact you"
  }
}

Let’s tweak our component a bit

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import Form from "@rjsf/core";

// ...

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

// ...

And here is the final look

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

1
2
3
4
const schema = {
  title: "Mad Field",
  type: "string",
};

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 ;)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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 + " - ")} />
  );
};

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

1
2
3
const widgets = {
  madTextWidget: MadTextWidget,
};

Finally, we can define the UI Schema

1
2
3
const uiSchema = {
  "ui:widget": "madTextWidget",
};

And the full code with the RJSF

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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"));

It will look like this

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const Form = JSONSchemaForm.default;
const schema = {
  type: "string",
  minLength: 5
};

const formData = "Hi";

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

Will end up looking like this

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.

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "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
}

See highlighted 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).

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
  "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
}

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

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.

Once again, no code, only JSON Schema modification

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
{
  "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"
            }
          }
        }
      ]
    }
  }
}

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!

Share on