Skip to content

Instantly share code, notes, and snippets.

@Evan-Kelly
Last active April 10, 2025 04:00
Show Gist options
  • Save Evan-Kelly/ddda8beea56ef3c8aad51677ffbbdee1 to your computer and use it in GitHub Desktop.
Save Evan-Kelly/ddda8beea56ef3c8aad51677ffbbdee1 to your computer and use it in GitHub Desktop.
A custom class for validating zod schemas. Supports validation of objects. arrays. partials, and individual fields. Emits a CustomZodError which wraps ZodError.
import { z } from 'zod';
/**
* Simplified ZodError with focused error formatting capabilities
*/
class SimplifiedZodError extends z.ZodError {
formatToSchema(schema: z.ZodType): any {
const formatted = this.format() as Record<string, any>;
if (schema instanceof z.ZodObject) {
const result: Record<string, any> = {};
const shape = schema.shape as Record<string, z.ZodType>;
for (const key in shape) {
if (formatted[key]?._errors?.length) {
result[key] = formatted[key]._errors[0];
} else if (formatted[key] && typeof formatted[key] === 'object') {
const nestedResult = this.extractErrorMessages(formatted[key]);
if (nestedResult) {
result[key] = nestedResult;
}
}
}
return Object.keys(result).length > 0 ? result : null;
}
if (schema instanceof z.ZodArray) {
return this.formatArrayErrors(formatted);
}
return formatted._errors?.[0] || null;
}
private extractErrorMessages(obj: Record<string, any>): any {
if (!obj || typeof obj !== 'object') {
return null;
}
if (obj._errors?.length) {
return obj._errors[0];
}
const result: Record<string, any> = {};
for (const key in obj) {
if (key === '_errors') continue;
if (obj[key]?._errors?.length) {
result[key] = obj[key]._errors[0];
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
const nestedResult = this.extractErrorMessages(obj[key]);
if (nestedResult) {
if (typeof nestedResult === 'object') {
Object.assign(result, nestedResult);
} else {
result[key] = nestedResult;
}
}
}
}
return Object.keys(result).length > 0 ? result : null;
}
getFieldError(path: string): string | null {
const parts = path.split('.');
const formatted = this.format() as Record<string, any>;
let current: Record<string, any> = formatted;
for (const part of parts) {
if (!current[part]) {
return null;
}
current = current[part];
}
if (current._errors?.length) {
return current._errors[0];
}
for (const key in current) {
if (key !== '_errors' && current[key]?._errors?.length) {
return current[key]._errors[0];
}
}
return null;
}
private formatArrayErrors(formatted: Record<string, any>): any {
const result: Record<string, any> = {};
const numericKeys = Object.keys(formatted)
.filter(key => !isNaN(Number(key)) && key !== '_errors');
if (numericKeys.length > 0) {
for (const key of numericKeys) {
const indexErrors = this.extractErrorMessages(formatted[key]);
if (indexErrors) {
result[key] = indexErrors;
}
}
return Object.keys(result).length > 0 ? result : null;
}
return formatted._errors?.[0] || null;
}
}
/**
* Configuration options for schema validator
*/
export interface SchemaValidatorOptions {
/** Optional custom partial schema (if not provided, will be inferred) */
partialSchema?: z.ZodType;
}
/**
* Helper function to create an error object
*/
function createError(options: {
statusCode: number;
message: string;
data?: any
}): Error {
const error = new Error(options.message);
(error as any).statusCode = options.statusCode;
(error as any).data = options.data;
return error;
}
/**
* Creates a schema validator with validation methods
* @param schema The Zod schema to validate against
* @param options Configuration options with optional custom partial schema
* @returns A validator object with validation methods
*/
export function createSchemaValidator<T extends z.ZodType>(
schema: T,
options?: SchemaValidatorOptions
): {
validate: (data: unknown) => z.infer<T>;
validatePartial: (data: unknown) => Partial<z.infer<T>>;
validateField: (data: unknown, fieldId: string) => string | null;
validateArray: (data: unknown[]) => Array<z.infer<T> extends Array<infer U> ? U : z.infer<T>>;
validatePartialArray: (data: unknown[]) => Array<Partial<z.infer<T> extends Array<infer U> ? U : z.infer<T>>>;
} {
// Get the appropriate partial schema
const getPartialSchema = (): z.ZodType => {
if (options?.partialSchema) {
return options.partialSchema;
}
if (schema instanceof z.ZodObject) {
return schema.partial();
}
return schema;
};
// Get the array item schema
const getArrayItemSchema = (): z.ZodType => {
if (schema instanceof z.ZodArray) {
return schema.element;
}
return schema;
};
// Get the partial array item schema - uses the custom partial schema if provided
const getPartialArrayItemSchema = (): z.ZodType => {
if (options?.partialSchema && schema instanceof z.ZodArray) {
// If the partial schema itself is an array, use its element
if (options.partialSchema instanceof z.ZodArray) {
return options.partialSchema.element;
}
// Otherwise, use the entire partial schema
return options.partialSchema;
}
const itemSchema = getArrayItemSchema();
if (itemSchema instanceof z.ZodObject) {
return itemSchema.partial();
}
return itemSchema;
};
// Error handlers
const handleValidationError = (
error: any,
schemaToFormat: z.ZodType,
statusCode = 422,
message = 'Validation failed'
): never => {
if (error instanceof z.ZodError) {
const customError = new SimplifiedZodError(error.issues);
throw createError({
statusCode,
message,
data: customError.formatToSchema(schemaToFormat)
});
}
throw error;
};
const handleArrayValidationError = (
error: any,
arraySchema: z.ZodType,
statusCode = 422,
message = 'Array validation failed'
): never => {
if (error instanceof z.ZodError) {
const customError = new SimplifiedZodError(error.issues);
throw createError({
statusCode,
message,
data: customError.formatToSchema(arraySchema)
});
}
throw error;
};
// The streamlined validator object with core methods
return {
validate: (data: unknown): z.infer<T> => {
try {
return schema.parse(data);
} catch (error) {
return handleValidationError(error, schema);
}
},
validatePartial: (data: unknown): Partial<z.infer<T>> => {
try {
const partialSchema = getPartialSchema();
return partialSchema.parse(data);
} catch (error) {
return handleValidationError(
error,
getPartialSchema(),
422,
'Partial validation failed'
);
}
},
validateField: (data: unknown, fieldId: string): string | null => {
try {
schema.parse(data);
return null;
} catch (error) {
if (error instanceof z.ZodError) {
const customError = new SimplifiedZodError(error.issues);
return customError.getFieldError(fieldId);
}
return 'Validation failed';
}
},
validateArray: (data: unknown[]): Array<z.infer<T> extends Array<infer U> ? U : z.infer<T>> => {
const itemSchema = getArrayItemSchema();
const arraySchema = z.array(itemSchema);
try {
return arraySchema.parse(data);
} catch (error) {
return handleArrayValidationError(
error,
arraySchema
);
}
},
validatePartialArray: (data: unknown[]): Array<Partial<z.infer<T> extends Array<infer U> ? U : z.infer<T>>> => {
const partialItemSchema = getPartialArrayItemSchema();
const partialArraySchema = z.array(partialItemSchema);
try {
return partialArraySchema.parse(data);
} catch (error) {
return handleArrayValidationError(
error,
partialArraySchema,
422,
'Partial array validation failed'
);
}
}
};
}
Sign in to join this conversation on GitHub.