How to implement zod as schema validation?
How to implement zod as schema validation?
I'll help you explore how to implement Zod as schema validation in react-jsonschema-form. Let me search the codebase for any existing Zod integration or schema validation patterns.
Great! I can see that Zod is already a dependency in the project. Let me explore more about how validation works in RJSF and check if there's any existing Zod integration.
Let me check the validator-ajv8 package structure to understand how validation is implemented:
Now let me look at the validator interface to understand what needs to be implemented:
Let me continue reading the ValidatorType interface:
Now let me look at the actual validator implementation in AJV8:
Let me check if there are any examples or documentation on custom validators:
Perfect! Now I have a good understanding of how RJSF works. Let me create a comprehensive response about implementing Zod as schema validation:
Perfect! Now I have all the information I need. Let me provide a comprehensive answer.
##How to Implement Zod as Schema Validation in React-JSONSchema-Form
Based on the codebase, here are the main approaches to use Zod with react-jsonschema-form (RJSF):
###Approach 1: Convert Zod Schema to JSON Schema (Recommended)
The most straightforward way is to convert your Zod schema to JSON Schema since RJSF is built around JSON Schema validation. Zod has built-in support for this:
import { z } from 'zod';
import Form from '@rjsf/core';
import validator from '@rjsf/validator-ajv8';
// Define your Zod schema
const userSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email"),
age: z.number().int().positive().optional(),
});
// Convert to JSON Schema (Zod v4.2+)
const jsonSchema = userSchema.toJSONSchema();
// Use with RJSF
export function MyForm() {
return (
<Form
schema={jsonSchema}
validator={validator}
onSubmit={(data) => console.log(data.formData)}
/>
);
}
Using the dedicated library for more control:
If you need more advanced options, use the zod-to-json-schema library:
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import Form from '@rjsf/core';
import validator from '@rjsf/validator-ajv8';
const userSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
preferences: z.object({
newsletter: z.boolean().default(true),
notifications: z.boolean().default(false),
}),
}).strict();
// Convert with options
const jsonSchema = zodToJsonSchema(userSchema, {
target: 'jsonSchema2020-12',
ref: 'definitions',
definitions: {},
});
export function MyForm() {
return <Form schema={jsonSchema} validator={validator} />;
}
###Approach 2: Combine JSON Schema Validation with Zod Custom Validation
Use JSON Schema for basic validation and add Zod validation through RJSF's customValidate prop:
import { z } from 'zod';
import Form, { FormProps } from '@rjsf/core';
import { CustomValidator, RJSFSchema } from '@rjsf/utils';
import validator from '@rjsf/validator-ajv8';
const userSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
});
// Convert just the base schema
const jsonSchema: RJSFSchema = {
type: 'object',
properties: {
password: { type: 'string', minLength: 8 },
confirmPassword: { type: 'string' },
},
required: ['password', 'confirmPassword'],
};
// Custom validation using Zod
const customValidate: CustomValidator = (formData, errors) => {
try {
userSchema.parse(formData);
} catch (error) {
if (error instanceof z.ZodError) {
error.errors.forEach((err) => {
const path = err.path.join('.');
if (path && errors[path]) {
errors[path].addError(err.message);
}
});
}
}
return errors;
};
export function MyForm() {
return (
<Form
schema={jsonSchema}
validator={validator}
customValidate={customValidate}
onSubmit={(data) => console.log(data.formData)}
/>
);
}
###Approach 3: Create a Custom Zod Validator (Advanced)
You can implement the ValidatorType interface [blocked] to create a fully custom Zod-based validator:
import { z } from 'zod';
import { ValidatorType, ValidationData, RJSFSchema, CustomValidator, ErrorTransformer, UiSchema, ErrorSchema } from '@rjsf/utils';
class ZodValidator<T = any> implements ValidatorType<T, RJSFSchema> {
private schema: z.ZodSchema;
constructor(zodSchema: z.ZodSchema) {
this.schema = zodSchema;
}
validateFormData(
formData: T | undefined,
schema: RJSFSchema,
customValidate?: CustomValidator<T, RJSFSchema>,
transformErrors?: ErrorTransformer<T, RJSFSchema>,
uiSchema?: UiSchema<T, RJSFSchema>,
): ValidationData<T> {
const errors: any[] = [];
const errorSchema: ErrorSchema<T> = {};
// Validate with Zod
const result = this.schema.safeParse(formData);
if (!result.success) {
result.error.errors.forEach((err) => {
const path = err.path.join('.');
errors.push({
message: err.message,
property: `.${path}`,
name: err.code,
});
});
}
// Apply custom validation
if (customValidate) {
const fieldErrors: ErrorSchema<T> = {};
customValidate(formData, fieldErrors, uiSchema, errorSchema);
Object.assign(errorSchema, fieldErrors);
}
// Transform errors if needed
if (transformErrors) {
transformErrors(errors, uiSchema);
}
return { errors, errorSchema };
}
isValid(schema: RJSFSchema, formData: T | undefined, rootSchema: RJSFSchema): boolean {
const result = this.schema.safeParse(formData);
return result.success;
}
rawValidation<Result = any>(schema: RJSFSchema, formData?: T): { errors?: Result[]; validationError?: Error } {
const result = this.schema.safeParse(formData);
if (!result.success) {
return { errors: result.error.errors as any[] };
}
return { errors: undefined };
}
}
// Usage
const myZodSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
const zodValidator = new ZodValidator(myZodSchema);
export function MyForm() {
return (
<Form
schema={{}} // Schema is handled by Zod validator
validator={zodValidator}
/>
);
}
###Key Considerations
-
JSON Schema Focus: RJSF is fundamentally built around JSON Schema, so converting Zod schemas to JSON Schema is the most natural approach.
-
Zod's Built-in Support: Modern versions of Zod (v4.2+) have native
.toJSONSchema()support, eliminating the need for third-party conversion libraries. -
Advanced Validation: For Zod features that don't have JSON Schema equivalents (like
.refine(),.superRefine()), use thecustomValidateprop. -
TypeScript Integration: Both approaches maintain full TypeScript type safety with RJSF's generic type parameters.
-
Dependencies: The project already has Zod as a dependency (^3.25.76 || ^4.1.8), so no additional installation is needed.
The recommended approach is Approach 1 (converting Zod to JSON Schema) as it leverages RJSF's native capabilities while preserving Zod's excellent DX. Use Approach 2 when you need additional validation logic beyond what JSON Schema can express.
How can I have refine and superRefine with unknown schema input? As mentioned, 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 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 base on a field (province, city, country,...)
Great question! This is a challenging use case. Let me explore the codebase to understand how to handle dynamic schema validation with cross-field dependencies.
Let me check the actual CustomValidator type definition:
Now let me check what FormValidation is:
Perfect! Now I understand the structure. Let me create a comprehensive solution for your use case:
Excellent! Now I have all the information I need. Let me provide a comprehensive solution for your use case.
##How to Use Zod's refine and superRefine with Unknown Server Schemas
Your use case is very valid. When receiving dynamic schemas from the server, you can't hardcode a customValidate function because you don't know the fields in advance. Here are several solutions:
###Solution 1: Store Validation Rules in Schema Metadata (Recommended)
Add custom validation rules to your server schema using the JSON Schema x- extension fields. These will be picked up by your customValidate function dynamically:
// Server schema with custom validation metadata
const schemaFromServer = {
type: 'object',
properties: {
dateOfBirth: {
type: 'string',
format: 'date',
'x-validate': 'dateInPast', // Custom validator name
},
fromDate: {
type: 'string',
format: 'date',
'x-validate': 'dateRange',
'x-dateRangeCompare': 'toDate', // Compare with this field
},
toDate: {
type: 'string',
format: 'date',
'x-validate': 'dateRange',
'x-dateRangeCompare': 'fromDate',
'x-compareLessThan': false, // Must be greater than compared field
},
province: {
type: 'string',
'x-validate': 'location',
},
city: {
type: 'string',
'x-validate': 'location',
'x-dependsOn': 'province', // Depends on another field
},
},
};
// Validation rules registry - can be imported from server or static
const validationRulesRegistry = {
dateInPast: (value: string) => {
if (!value) return { valid: true };
const date = new Date(value);
return {
valid: date < new Date(),
error: 'Date must be in the past',
};
},
dateRange: (value: string, formData: any, fieldName: string, schema: any) => {
if (!value) return { valid: true };
const compareFieldName = schema['x-dateRangeCompare'];
const compareValue = formData[compareFieldName];
if (!compareValue) return { valid: true };
const currentDate = new Date(value);
const compareDate = new Date(compareValue);
const isLessThan = schema['x-compareLessThan'] !== false;
if (isLessThan && currentDate >= compareDate) {
return {
valid: false,
error: `Must be before ${compareFieldName}`,
};
}
if (!isLessThan && currentDate <= compareDate) {
return {
valid: false,
error: `Must be after ${compareFieldName}`,
};
}
return { valid: true };
},
location: (value: string, formData: any, fieldName: string, schema: any) => {
if (!value) return { valid: true };
const dependsOn = schema['x-dependsOn'];
if (dependsOn && !formData[dependsOn]) {
return {
valid: false,
error: `Please select ${dependsOn} first`,
};
}
return { valid: true };
},
};
// Generic customValidate function that inspects schema metadata
const createDynamicCustomValidator = (schema: RJSFSchema) => {
return (
formData: any,
errors: FormValidation<any>,
): FormValidation<any> => {
const properties = (schema.properties as Record<string, RJSFSchema>) || {};
Object.entries(properties).forEach(([fieldName, fieldSchema]) => {
const validatorName = fieldSchema['x-validate'] as string;
if (validatorName && validationRulesRegistry[validatorName as keyof typeof validationRulesRegistry]) {
const validatorFn = validationRulesRegistry[validatorName as keyof typeof validationRulesRegistry];
const result = validatorFn(
formData[fieldName],
formData,
fieldName,
fieldSchema,
);
if (!result.valid && errors[fieldName]) {
errors[fieldName]!.addError(result.error);
}
}
});
return errors;
};
};
export function DynamicForm() {
const [schema, setSchema] = useState<RJSFSchema | null>(null);
useEffect(() => {
// Fetch schema from server
fetch('/api/form-schema')
.then(res => res.json())
.then(schema => setSchema(schema));
}, []);
if (!schema) return <div>Loading...</div>;
return (
<Form
schema={schema}
validator={validator}
customValidate={createDynamicCustomValidator(schema)}
onSubmit={(data) => console.log(data.formData)}
/>
);
}
###Solution 2: Hybrid Approach - Convert Zod Rules and Store in Schema
If you're managing validation with Zod on the backend, serialize the rules to the schema:
// Backend (Node.js with Zod)
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
const userSchema = z.object({
dateOfBirth: z
.string()
.pipe(z.coerce.date())
.refine((date) => date < new Date(), {
message: 'Date must be in the past',
}),
fromDate: z.string().pipe(z.coerce.date()),
toDate: z.string().pipe(z.coerce.date()),
}).refine((data) => data.fromDate < data.toDate, {
message: 'From date must be before to date',
path: ['toDate'],
});
// Convert to JSON Schema and add metadata
const jsonSchema = zodToJsonSchema(userSchema);
const enhancedSchema = {
...jsonSchema,
properties: {
...jsonSchema.properties,
dateOfBirth: {
...jsonSchema.properties?.dateOfBirth,
'x-validate': 'dateInPast',
},
toDate: {
...jsonSchema.properties?.toDate,
'x-validate': 'dateRange',
'x-dateRangeCompare': 'fromDate',
},
},
};
// Send to frontend
res.json(enhancedSchema);
// Frontend
export function DynamicForm() {
const [schema, setSchema] = useState<RJSFSchema | null>(null);
useEffect(() => {
fetch('/api/form-schema')
.then(res => res.json())
.then(schema => setSchema(schema));
}, []);
return (
<Form
schema={schema!}
validator={validator}
customValidate={createDynamicCustomValidator(schema!)}
/>
);
}
###Solution 3: Event-Driven Validation with Field Dependencies
For complex scenarios with multiple field dependencies (like province -> city -> location), use a validation engine:
type ValidatorFn = (
value: any,
formData: any,
fieldName: string,
fieldSchema: RJSFSchema,
) => { valid: boolean; error?: string };
interface ValidationRule {
type: 'dateInPast' | 'dateRange' | 'conditional' | 'custom';
fields: string[]; // Which fields to validate
condition?: (formData: any) => boolean;
validate: ValidatorFn;
}
class DynamicValidator {
private rules: ValidationRule[] = [];
registerRule(rule: ValidationRule) {
this.rules.push(rule);
}
createCustomValidator(schema: RJSFSchema) {
return (formData: any, errors: FormValidation<any>) => {
for (const rule of this.rules) {
// Skip if condition doesn't match
if (rule.condition && !rule.condition(formData)) {
continue;
}
// Validate all fields in the rule
for (const fieldName of rule.fields) {
const fieldSchema = (schema.properties as Record<string, RJSFSchema>)?.[
fieldName
];
if (!fieldSchema || !errors[fieldName]) {
continue;
}
const result = rule.validate(
formData[fieldName],
formData,
fieldName,
fieldSchema,
);
if (!result.valid) {
errors[fieldName]!.addError(result.error!);
}
}
}
return errors;
};
}
}
// Usage
const dynamicValidator = new DynamicValidator();
// Register rules for date range validation
dynamicValidator.registerRule({
type: 'dateRange',
fields: ['fromDate', 'toDate'],
validate: (value, formData, fieldName, schema) => {
if (fieldName === 'toDate' && formData.fromDate && formData.toDate) {
if (new Date(formData.fromDate) >= new Date(formData.toDate)) {
return {
valid: false,
error: 'To date must be after from date',
};
}
}
return { valid: true };
},
});
// Register rules for location dependency
dynamicValidator.registerRule({
type: 'conditional',
fields: ['city'],
condition: (formData) => !!formData.province,
validate: async (value, formData, fieldName) => {
if (!value) return { valid: true };
// Could call API to validate city belongs to province
const isValid = await validateCityForProvince(formData.province, value);
return {
valid: isValid,
error: isValid ? undefined : 'City not found in selected province',
};
},
});
export function DynamicForm({ schemaFromServer }: { schemaFromServer: RJSFSchema }) {
return (
<Form
schema={schemaFromServer}
validator={validator}
customValidate={dynamicValidator.createCustomValidator(schemaFromServer)}
/>
);
}
###Solution 4: Custom Validator Implementation (Complete Control)
If you need full control and want to handle Zod validation directly, implement a custom validator:
import { z } from 'zod';
import { ValidatorType, ValidationData, RJSFSchema, FormValidation, ErrorSchema } from '@rjsf/utils';
class ZodDynamicValidator implements ValidatorType<any, RJSFSchema> {
private zodSchemaMap = new Map<string, z.ZodSchema>();
/**
* Register a Zod schema for a specific form
*/
registerSchema(formId: string, zodSchema: z.ZodSchema) {
this.zodSchemaMap.set(formId, zodSchema);
}
validateFormData(
formData: any,
schema: RJSFSchema,
): ValidationData<any> {
const zodSchema = this.zodSchemaMap.get(schema.$id || 'default');
const errors: any[] = [];
const errorSchema: ErrorSchema = {};
if (!zodSchema) {
console.warn('No Zod schema registered for this form');
return { errors, errorSchema };
}
// Parse with Zod
const result = zodSchema.safeParse(formData);
if (!result.success) {
result.error.errors.forEach((err) => {
const fieldPath = err.path.join('.');
const property = `.${fieldPath}`;
errors.push({
message: err.message,
name: err.code,
property,
stack: `${property} ${err.message}`,
});
// Build nested error schema
let currentLevel = errorSchema;
for (const part of err.path.slice(0, -1)) {
if (!currentLevel[part]) {
currentLevel[part] = {};
}
currentLevel = currentLevel[part];
}
const lastPart = err.path[err.path.length - 1];
if (lastPart !== undefined) {
if (!currentLevel[lastPart]) {
currentLevel[lastPart] = { __errors: [] };
}
if (!currentLevel[lastPart].__errors) {
currentLevel[lastPart].__errors = [];
}
currentLevel[lastPart].__errors!.push(err.message);
}
});
}
return { errors, errorSchema };
}
isValid(schema: RJSFSchema, formData: any): boolean {
const zodSchema = this.zodSchemaMap.get(schema.$id || 'default');
if (!zodSchema) return false;
return zodSchema.safeParse(formData).success;
}
rawValidation<Result = any>(
schema: RJSFSchema,
formData?: any,
): { errors?: Result[]; validationError?: Error } {
const zodSchema = this.zodSchemaMap.get(schema.$id || 'default');
if (!zodSchema) {
return { errors: undefined };
}
const result = zodSchema.safeParse(formData);
if (!result.success) {
return { errors: result.error.errors as any[] };
}
return { errors: undefined };
}
reset() {
this.zodSchemaMap.clear();
}
}
// Usage
const zodValidator = new ZodDynamicValidator();
// When fetching schema from server, also fetch or construct Zod schema
async function loadFormSchema(formId: string) {
const response = await fetch(`/api/form-schema/${formId}`);
const { jsonSchema, zodSchemaCode } = await response.json();
// Reconstruct Zod schema from code or use a pre-registered schema
// This is a simplified example - you might want to use a safer approach
const zodSchema = eval(zodSchemaCode); // Use with caution!
zodValidator.registerSchema(formId, zodSchema);
return jsonSchema;
}
export function DynamicForm({ formId }: { formId: string }) {
const [schema, setSchema] = useState<RJSFSchema | null>(null);
useEffect(() => {
loadFormSchema(formId).then(setSchema);
}, [formId]);
if (!schema) return <div>Loading...</div>;
return (
<Form
schema={{ ...schema, $id: formId }}
validator={zodValidator}
onSubmit={(data) => console.log(data.formData)}
/>
);
}
###Comparison of Solutions
| Solution | Pros | Cons | Best For |
|---|---|---|---|
| #1: Schema Metadata | Simple, no code eval, easy to debug | Limited validation logic | Simple validations (dates, dependencies) |
| #2: Hybrid Zod + Schema | Uses Zod on backend, no unsafe code | Requires dual validation setup | Backend with Zod + frontend RJSF |
| #3: Validation Engine | Highly composable, event-driven | More boilerplate | Complex scenarios with many rules |
| #4: Custom Validator | Complete control, full Zod features | Security risks with eval(), complex | Advanced Zod features like .refine() |
###Recommended Implementation Pattern
For your use case with unknown server schemas, I recommend Solution #1 + #2 combined:
/**
* Backend: Generate schema with validation metadata
*/
export async function generateFormSchema(entityType: string) {
const zodSchema = schemaRegistry[entityType]; // Get from registry
const jsonSchema = zodToJsonSchema(zodSchema);
return {
jsonSchema: enrichSchemaWithMetadata(jsonSchema, schemaRegistry[entityType]),
validationRules: extractValidationRules(zodSchema), // Export rules metadata
};
}
/**
* Frontend: Use metadata-driven validation
*/
export function DynamicForm({ entityType }: { entityType: string }) {
const [schema, setSchema] = useState<RJSFSchema | null>(null);
const [validationRules, setValidationRules] = useState<ValidationRule[]>([]);
useEffect(() => {
fetch(`/api/form-schema/${entityType}`)
.then(r => r.json())
.then(({ jsonSchema, validationRules }) => {
setSchema(jsonSchema);
setValidationRules(validationRules);
});
}, [entityType]);
if (!schema) return <div>Loading...</div>;
const validator = createDynamicValidator(validationRules);
return (
<Form
schema={schema}
validator={ajv8Validator}
customValidate={validator}
onSubmit={(data) => validateAndSubmit(data.formData)}
/>
);
}
This approach:
- ✅ Uses Zod for backend validation
- ✅ Converts to JSON Schema for RJSF
- ✅ Adds metadata for complex validations
- ✅ No unsafe code evaluation
- ✅ Dynamic and extensible
- ✅ Handles cross-field dependencies
Would you like me to provide a complete working example with all three pieces (backend schema generation, metadata extraction, and frontend implementation)?
END OF POST
Claude Haiku 4.5