React JSON Schema Form
Tags: react, javascript, rjsf, jsonschema
Published: 2021-05-06 09:31:33

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](https://valerii-udodov.com/posts/making-strongly-typed-language-a-bit-more-loose-with-json-schema/#the-jsonschema) 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

```json
{
  "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

```json
{
  "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
    }
  }
}
```

___
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...

```react
import Form from "@rjsf/core";

// ...

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

// ...
```

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0eg62ok1rljqtlzepnxi.png)

### 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](https://react-jsonschema-form.readthedocs.io/en/latest/api-reference/uiSchema/#uischema) 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. 

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

```

Let's tweak our component a bit

```react
import Form from "@rjsf/core";

// ...

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

// ...
```

And here is the final look

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1htjlp1kbh6uude1b14o.png)

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](https://react-jsonschema-form.readthedocs.io/en/latest/advanced-customization/custom-widgets-fields/)

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

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

```react
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

```js
const widgets = {
  madTextWidget: MadTextWidget
}
```

Finally, we can define the UI Schema

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

And the full code with the RJSF

```react
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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9otdcy4sghzv8ngunz8q.png)

Here, [try it yourself](https://codepen.io/vudodov/pen/LYWPKPa). 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](https://react-jsonschema-form.readthedocs.io/en/latest/advanced-customization/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](https://react-jsonschema-form.readthedocs.io/en/latest/advanced-customization/custom-themes/) 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

```react
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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d9mqrpqkriq27ann4b1a.png)

Of course, we can [re-define messages](https://react-jsonschema-form.readthedocs.io/en/latest/usage/validation/), 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](https://github.com/ajv-validator/ajv). 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](https://json-schema.org/understanding-json-schema/reference/combining.html). 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.

```json
{
  "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 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tsfg8sairbbo2fqvceww.gif)

### 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.

```json
{
  "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).

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7j7hbqr7ous2z83bpf40.gif)

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dekjle6kfv45joidff30.gif)

Once again, no code, only JSON Schema modification

```json
{
  "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](https://react-jsonschema-form.readthedocs.io/en/latest/) for more insights and examples, [GitHub repository](https://github.com/rjsf-team/react-jsonschema-form) for undocumented goodies and [live playground](https://rjsf-team.github.io/react-jsonschema-form/) 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!