How to implement schema-driven dynamic validation?
How can I schema-driven dynamic validation. As known, customValidate extends validation with JavaScript code based on the current form data. However, my use case is for unknown schema from the server through GET request. The customValidate might not know which field to validate. The refine, super refine can be use when I need to validate Date of birth field (require old date), or from-to date picker when require from date must lower then to date, or place picker where you need to filter out based on a field (province, city, country,...)
Perfect! Now I have all the information I need. Let me provide a comprehensive answer to the user.
##Schema-Driven Dynamic Validation in react-jsonschema-form
Based on the source code, I can show you how to implement schema-driven dynamic validation that addresses your use case of unknown schemas from the server. The key is understanding how customValidate works and how to make it dynamic.
###The Core Mechanism: customValidate
The customValidate function is the primary mechanism for cross-field validation in react-jsonschema-form. Here's the type definition from the source:
export type CustomValidator<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any> = (
formData: T | undefined,
errors: FormValidation<T>,
uiSchema?: UiSchema<T, S, F>,
errorSchema?: ErrorSchema<T>,
) => FormValidation<T>;
Key Points:
- formData: The entire form data object (with all fields including undefined values)
- errors: A
FormValidationobject that mirrors your form's structure, with anaddError()method for each field - uiSchema: Can be used to derive additional validation context
- errorSchema: Contains errors from JSON Schema validation - useful for conditional custom validation
###Solution for Schema-Driven Dynamic Validation
Here's how to implement dynamic validation for unknown schemas from the server:
import { Form } from '@rjsf/core';
import { CustomValidator, RJSFSchema } from '@rjsf/utils';
import validator from '@rjsf/validator-ajv8';
/**
* Schema-driven custom validator that reads validation rules from the schema itself
*/
const dynamicCustomValidate: CustomValidator = (formData, errors, uiSchema, errorSchema) => {
// Get the schema from somewhere (can be part of formContext or passed via closure)
// This allows you to define validation rules in your schema
if (!formData) return errors;
// Example 1: Validate Date of Birth (must be older than 18)
if (formData.dateOfBirth) {
const age = calculateAge(new Date(formData.dateOfBirth));
if (age < 18) {
errors.dateOfBirth?.addError('Must be at least 18 years old');
}
}
// Example 2: Cross-field date range validation (from date < to date)
if (formData.fromDate && formData.toDate) {
const fromDate = new Date(formData.fromDate);
const toDate = new Date(formData.toDate);
if (fromDate >= toDate) {
errors.fromDate?.addError('From date must be before To date');
}
}
// Example 3: Dependent field validation (city depends on province)
if (formData.province && formData.city) {
const validCities = getValidCities(formData.province);
if (!validCities.includes(formData.city)) {
errors.city?.addError(`${formData.city} is not valid for province ${formData.province}`);
}
}
return errors;
};
function calculateAge(birthDate: Date): number {
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age;
}
function getValidCities(province: string): string[] {
// This could come from your API/database
const cityMap: Record<string, string[]> = {
'Ontario': ['Toronto', 'Ottawa', 'Hamilton'],
'Quebec': ['Montreal', 'Quebec City', 'Gatineau'],
// ... etc
};
return cityMap[province] || [];
}
// Usage
const schema: RJSFSchema = {
type: 'object',
properties: {
dateOfBirth: { type: 'string', format: 'date' },
fromDate: { type: 'string', format: 'date' },
toDate: { type: 'string', format: 'date' },
province: { type: 'string', enum: ['Ontario', 'Quebec'] },
city: { type: 'string' },
},
};
render(
<Form
schema={schema}
validator={validator}
customValidate={dynamicCustomValidate}
/>,
document.getElementById('app')
);
###Advanced: Schema-Driven Validation Rules
For truly dynamic schemas from the server, you can embed validation rules in the schema itself using custom properties:
const serverSchema: RJSFSchema = {
type: 'object',
properties: {
dateOfBirth: {
type: 'string',
format: 'date',
'x-validate': 'minAge:18', // Custom validation rule
},
fromDate: {
type: 'string',
format: 'date',
'x-validate': 'before:toDate', // Custom validation rule
},
toDate: {
type: 'string',
format: 'date',
},
province: {
type: 'string',
'x-validate': 'dependsOn:city', // Custom validation rule
},
city: {
type: 'string',
'x-validate': 'filteredBy:province', // Custom validation rule
},
},
};
/**
* Generic schema-driven validator that interprets custom validation rules from schema
*/
function createDynamicValidator(schema: RJSFSchema): CustomValidator {
return (formData, errors) => {
if (!formData) return errors;
// Iterate through schema properties and apply custom validations
Object.entries(schema.properties || {}).forEach(([fieldName, fieldSchema]: [string, any]) => {
const validationRule = fieldSchema['x-validate'];
if (!validationRule) return;
const fieldValue = formData[fieldName];
const fieldErrors = errors[fieldName];
// Parse and apply validation rules
if (validationRule.startsWith('minAge:')) {
const minAge = parseInt(validationRule.split(':')[1]);
const age = calculateAge(new Date(fieldValue));
if (age < minAge) {
fieldErrors?.addError(`Must be at least ${minAge} years old`);
}
}
else if (validationRule.startsWith('before:')) {
const beforeField = validationRule.split(':')[1];
const beforeValue = formData[beforeField];
if (new Date(fieldValue) >= new Date(beforeValue)) {
fieldErrors?.addError(`Must be before ${beforeField}`);
}
}
else if (validationRule.startsWith('filteredBy:')) {
const dependsOnField = validationRule.split(':')[1];
const dependsOnValue = formData[dependsOnField];
const validValues = getValidCities(dependsOnValue);
if (!validValues.includes(fieldValue)) {
fieldErrors?.addError(`Not valid for ${dependsOnField}: ${dependsOnValue}`);
}
}
});
return errors;
};
}
// Usage with dynamic schema
const schema = await fetch('/api/schema').then(r => r.json());
const customValidate = createDynamicValidator(schema);
render(
<Form
schema={schema}
validator={validator}
customValidate={customValidate}
/>,
document.getElementById('app')
);
###Accessing the Schema Inside customValidate
If you need access to the schema within your validator, you have several options:
Option 1: Use a closure (recommended for unknown schemas)
function createValidatorForSchema(schema: RJSFSchema): CustomValidator {
return (formData, errors, uiSchema, errorSchema) => {
// Schema is accessible here via closure
// Apply validation rules based on schema
return errors;
};
}
const customValidate = createValidatorForSchema(dynamicSchema);
Option 2: Store schema in formContext
<Form
schema={schema}
validator={validator}
formContext={{ schema }}
customValidate={(formData, errors, uiSchema, errorSchema) => {
// Access via formContext if needed
return errors;
}}
/>
Option 3: Use the errorSchema parameter
The fourth parameter errorSchema contains the results of JSON Schema validation, which can help you determine which fields need additional validation:
const customValidate: CustomValidator = (formData, errors, uiSchema, errorSchema) => {
// Only apply custom validation if JSON schema validation passed
if (!errorSchema?.['dateOfBirth']?.__errors?.length) {
// Perform additional custom validation
}
return errors;
};
###Key Advantages
- Handles unknown schemas: Uses data-driven validation rules embedded in the schema
- Cross-field validation: Full access to all form data for complex validations
- Runs after schema validation: Complements JSON Schema validation without replacing it
- Flexible error handling: Can add errors to any field path in the form structure
- Performance: Runs synchronously; consider async validation if needed
###Source Code References
- CustomValidator type:
packages/utils/src/types.ts(line 1155) - Error handling:
packages/utils/src/createErrorHandler.ts - Validation processing:
packages/validator-ajv8/src/processRawValidationErrors.ts - Playground example:
packages/playground/src/samples/validation.ts
This approach gives you the flexibility to handle any validation scenario with unknown schemas from the server!
END OF POST