In the previous article, I showed how I built a dynamic form renderer using JSON configuration in React + Formik.
Rendering the form fields turned out to be the easy part. The real challenge started when I needed validation.
In a traditional form, creating a Yup schema is straightforward because the fields are known in advance.
const validationSchema = yup.object({
firstName: yup.string().required('First Name is required'),
email: yup.string().email('Invalid Email'),
});
But in a dynamic form system, fields are generated from configuration. That means validation rules also need to come from configuration.
Hardcoding validation defeats the purpose of having a schema-driven form.
In this article, I'll show how I built dynamic Yup validation schemas directly from JSON configuration.
This is Part 2 of my "Building Dynamic Forms in React" series.
Recap
In Part 1, we built a dynamic form renderer using a schema like this:
{
field: 'firstName',
label: 'First Name',
type: 'text',
colWidth: 6
}
Now we need to extend the schema to support validation metadata.
Extending the Configuration
I wanted validation to be completely controlled by configuration. So I introduced properties like:
- isMandatory
- min
- max
- regex
Example:
{
"field": "firstName",
"label": "First Name",
"type": "text",
"isMandatory": true,
"min": 3,
"max": 50
}
For email:
{
"field": "email",
"label": "Email",
"type": "email",
"isMandatory": true,
"regex": /^[^\s@]+@[^\s@]+\.[^\s@]+$/
}
Now both the UI and validation rules live in the same configuration.
Why This Matters
Without dynamic validation generation, every new field requires changes in two places:
- Form configuration
- Validation schema
This quickly becomes difficult to maintain.
The goal is:
Add a field to configuration → validation should automatically work.
Building the Validation Generator
The idea is simple:
Loop through every field and build a Yup schema dynamically.
Something like:
export const buildValidationSchema = (schema) => {
const validationFields = {};
Object.values(schema).forEach((fields) => {
fields.forEach((field) => {
let validator = Yup.string();
validationFields[field.field] = validator;
});
});
return Yup.object(validationFields);
};
At this point, every field is a string validator.
Now we can start adding rules.
Handling Required Fields
The first rule I implemented was mandatory field validation.
if (field.isMandatory) {
validator = validator.required(
`${field.label} is required`
);
}
This allows the schema to drive required field validation automatically.
{ field: 'firstName', isMandatory: true }
No additional Yup code required.
Handling Minimum and Maximum Length
Next, I added support for minimum and maximum length validation.
if (field.min) {
validator = validator.min(field.min, `${field.label} must be at least ${field.min} characters`);
}
if (field.max) {
validator = validator.max(field.max, `${field.label} must be at most ${field.max} characters`);
}
Configuration:
{ "field": "firstName", "min": 2, "max": 50 }
Generated validation:
Yup.string().min(2).max(50)
Handling Regex Validation
Regex support became useful for:
- email validation
- phone numbers
- custom business rules
Example:
if (field.regex) {
validator = validator.matches(field.regex, `${field.label} format is invalid`);
}
Configuration:
{ "field": "phone", "regex": /^[0-9]{10}$/ }
Generated validation:
Yup.string().matches(/^[0-9]{10}$/)
Putting Everything Together
The final generator looked something like this:
export const buildValidationSchema = (schema) => {
const validationFields = {};
Object.values(schema).forEach((fields) => {
fields.forEach((field) => {
let validator = Yup.string();
if (field.isMandatory) {
validator = validator.required(`${field.label} is required`);
}
if (field.min) {
validator = validator.min(field.min, `${field.label} must contain at least ${field.min} characters`);
}
if (field.max) {
validator = validator.max(field.max, `${field.label} cannot exceed ${field.max} characters`);
}
if (field.regex) {
validator = validator.matches(field.regex, `${field.label} format is invalid`);
}
validationFields[field.field] = validator;
});
});
return Yup.object(validationFields);
};
Now validation automatically adapts whenever configuration changes.
Benefits of This Approach
Single Source of Truth
Validation and rendering stay in the same configuration.
Easier Maintenance
Adding a new field requires only configuration changes.
Scalable Architecture
Works well when forms become:
- API-driven
- configurable
- multi-step
- enterprise-scale
Reduced Boilerplate
No more manually updating large Yup schemas every time a field changes.
Challenges I Encountered
While building this, I quickly realized validation isn't always this simple.
Real-world forms often need:
- conditional validation
- dependent fields
- dynamic child fields
- API-driven validation rules
Those scenarios require more advanced schema generation.
What's Next?
So far we've covered:
✅ Part 1: Rendering Dynamic Forms from JSON Configuration
✅ Part 2: Generating Dynamic Yup Validation Schemas
In the next article, I'll cover one of the most interesting challenges I faced:
👉 Handling Conditional Child Fields and Dynamic Validation Dependencies
This is where dynamic forms start becoming much more powerful—and much more complex.
Top comments (1)
Hey Folks👋
I'd love to hear how others are handling dynamic validations in their applications. If you're building something similar or would like to see the complete implementation, let me know in the comments 🚀