Skip to main content

JSON Forms Data Validation

Specific fields of a JSON Forms config variable can have validation rules applied. For example, you can check that an input matches a regular expression, ensuring that it is an email address or phone number, or you can ensure a date is within a certain range.

But, not all data validation can be done with regex and min/max rules. Sometimes it's handy to examine the form in its entirety for correctness. For example, you may want to ensure that a Salesforce field mapper contains a one-to-one field mapping (so a customer can't map "Phone" to both "Cell Phone" and "Work Phone" fields, etc).

One easy way to tackle JSON Forms form validation is to feed the results of a JSON Form config variable into a subsequent JSON Form data source which validates that the data with JavaScript rules you write. This "validator" form can show helpful, human-readable error messages and prevent a user from completing a config wizard until they've corrected their mistakes.

In this example, we'll validate a simple JSON Form.

Our example JSON Form

For this example, we have a simple form that contains:

  1. A field mapper mapping "source" fields to "destination" fields. For example, you can map Source Option 2 to Destination Option 5, etc. It's important that these mappings are unique. So, you can't map both Source Option 2 and Source Option 3 to Destination Option 5.
  2. A number input representing a person's age. While you could validate a number like this with minimum and maximum input validators, we'll show how to validate it with JavaScript here.
Example JSON Form data source
const myJsonForm = dataSource({
dataSourceType: "jsonForm",
display: {
label: "My JSON Form",
description: "A JSON form for testing",
},
inputs: {},
perform: async () => {
const schema = {
type: "object",
properties: {
mappings: {
type: "array",
items: {
type: "object",
properties: {
source: {
type: "string",
enum: [
"Source Option 1",
"Source Option 2",
"Source Option 3",
"Source Option 4",
"Source Option 5",
],
},
destination: {
type: "string",
enum: [
"Destination Option 1",
"Destination Option 2",
"Destination Option 3",
"Destination Option 4",
"Destination Option 5",
],
},
},
},
required: ["source", "destination"],
},
age: {
type: "integer",
},
},
};
const uiSchema = {
type: "VerticalLayout",
elements: [
{
type: "Control",
scope: "#/properties/mappings",
},
{
type: "Control",
scope: "#/properties/age",
},
],
};
return Promise.resolve({ result: { schema, uiSchema } });
},
});
Examle JSON Form with field mapping

Our JSON Forms config variable will yield an object that looks like this:

{
"mappings": [
{
"source": "Source Option 1",
"destination": "Destination Option 1"
},
{
"source": "Source Option 2",
"destination": "Destination Option 5"
},
{
"source": "Source Option 2",
"destination": "Destination Option 3"
},
{
"source": "Source Option 4",
"destination": "Destination Option 3"
}
],
"age": -5
}

Our example validator form

Within our form validator we want to:

  1. Verify that each source field was selected at most once.
  2. Verify that each destination field was select at most once.
  3. Verify that age was a positive number no greater than 130.

If any of these checks fail, we want to display an error and disallow a user from continuing (note the disabled "Finish" button).

Failed JSON Forms validation

If all checks pass, we want to display confirmation that their data looks correct, and allow the user to continue.

Passed JSON Forms validation

In our validator code below, we pass our previous JSON Form's results to our "validator" data source. We initialize an array, errors to []. Then, if we detect bad data in the form that is being processed, we push error messages onto our errors array.

If errors is empty at the end of the function, we display a JSON Form with a label that says ✅ No errors found, and the user is able to click the "finish" button in the config wizard.

If errors contains error messages, those messages like ❌ Age must be a positive number are displayed in the config wizard. The form then requires an invisible field, isInvalid, which cannot be set because it is invisible. This prevents a user from clicking "Finish" when errors are present.

Validation JSON Form
interface JsonFormData {
mappings: { source: string; destination: string }[];
age: number;
}

const myValidator = dataSource({
dataSourceType: "jsonForm",
display: {
label: "Validator",
description: "Validates previous JSON form",
},
inputs: {
data: input({
label: "Data",
type: "data",
required: true,
clean: (value) => value as JsonFormData,
}),
},
perform: async (context, { data }) => {
const { mappings, age } = data;

// Initialize with an empty set of errors
const errors: string[] = [];

if ((mappings || []).length === 0) {
// If the user submitted no mappings, add an error
errors.push("❌ At least one mapping is required");
} else {
mappings.forEach((mapping, index) => {
// If multiple source fields are mapped to a single destination, add an error
if (mappings.findIndex((m) => m.source === mapping.source) !== index) {
errors.push(
`❌ Duplicate source of "${mapping.source}" selected. Only use each source once.`,
);
}
// If multiple destination fields were mapped to a single source, add an error
if (
mappings.findIndex((m) => m.destination === mapping.destination) !==
index
) {
errors.push(
`❌ Duplicate destination of "${mapping.destination}" selected. Only use each destination once.`,
);
}
});
}

// Add an error if there is no age, or the age is too high or low
if (age === undefined) {
errors.push("❌ You must specify an age");
} else {
if (age < 0) {
errors.push("❌ Age must be a positive number");
}
if (age > 130) {
errors.push("❌ Nobody is that old.");
}
}

// If any errors were added to the errors array, return a series of labels displaying the errors
if (errors.length) {
return Promise.resolve({
result: {
schema: {
type: "object",
properties: {
isInvalid: {
type: "string",
},
},
// Add an invisible, but required, input to prevent the "Finish" button from being clickable
required: ["isInvalid"],
},
uiSchema: {
type: "VerticalLayout",
elements: errors.map((error) => ({
type: "Label",
text: `Error: ${error}`,
})),
},
},
});
} else {
// If no errors were present, display a single affirmative label and allow a user to continue
return Promise.resolve({
result: {
schema: {
type: "object",
properties: {},
},
uiSchema: {
type: "VerticalLayout",
elements: [{ type: "Label", text: "✅ No errors found" }],
},
},
});
}
},
});