Schema Validation
Version: 1.3.0 | Status: Stable
Overview
ZON includes runtime schema validation to ensure data integrity and type safety when working with LLM outputs or untrusted data sources.
Quick Start
import { zon, validate } from 'zon-format';
// Define schema
const UserSchema = zon.object({
name: zon.string(),
age: zon.number(),
email: zon.string().email()
});
// Validate data
const result = validate(zonOutput, UserSchema);
if (result.success) {
console.log(result.data); // { name: 'Alice', age: 30, email: '...' }
} else {
console.error(result.errors);
}
Schema Types
Primitives
// String
zon.string()
zon.string().min(3)
zon.string().max(100)
zon.string().email()
zon.string().url()
zon.string().uuid()
zon.string().regex(/^[A-Z]+$/)
zon.string().datetime() // ISO 8601
zon.string().date() // YYYY-MM-DD
zon.string().time() // HH:MM:SS
// Number
zon.number()
zon.number().min(0)
zon.number().max(100)
zon.number().int() // Integer only
// Boolean
zon.boolean()
// Null
zon.null()
// Literal
zon.literal('active')
zon.literal(42)
zon.literal(true)
Objects
const PersonSchema = zon.object({
name: zon.string(),
age: zon.number(),
address: zon.object({
street: zon.string(),
city: zon.string(),
zip: zon.string()
})
});
Arrays
// Array of strings
zon.array(zon.string())
// Array of objects
zon.array(zon.object({
id: zon.number(),
name: zon.string()
}))
// Array with min/max length
zon.array(zon.string()).min(1).max(10)
Enums
const RoleSchema = zon.enum(['admin', 'user', 'guest']);
// Validation
validate('admin', RoleSchema); // Success
validate('superadmin', RoleSchema); // Error
Optional & Nullable
const Schema = zon.object({
required: zon.string(),
optional: zon.string().optional(), // Can be undefined
nullable: zon.string().nullable(), // Can be null
both: zon.string().optional().nullable(), // Can be undefined or null
withDefault: zon.string().default("guest") // Defaults to "guest" if undefined
});
LLM Guardrails
Self-Correcting Prompts
Feed validation errors back to the LLM for self-correction:
const ProductSchema = zon.object({
name: zon.string(),
price: zon.number().min(0),
category: zon.enum(['Electronics', 'Books', 'Clothing'])
});
let attempts = 0;
let result;
while (attempts < 3) {
const llmOutput = await callLLM(prompt);
result = validate(llmOutput, ProductSchema);
if (result.success) {
break;
}
// Add errors to next prompt
prompt += `\n\nPrevious errors:\n${result.errors.join('\n')}\nPlease fix and try again.`;
attempts++;
}
if (result.success) {
console.log('Valid data:', result.data);
} else {
console.error('Failed after 3 attempts');
}
Prompt Generation
Generate schema documentation for system prompts:
const UserSchema = zon.object({
name: zon.string().describe("User's full name"),
age: zon.number().describe("Age in years"),
role: zon.enum(['admin', 'user']).describe("Access level")
});
const prompt = UserSchema.toPrompt();
console.log(prompt);
/*
object:
- name: string - User's full name
- age: number - Age in years
- role: enum(admin, user) - Access level
*/
Use in system messages:
const systemMessage = `
You are an API. Respond in ZON format with this structure:
${UserSchema.toPrompt()}
`;
Common Patterns
API Response Validation
const ApiResponseSchema = zon.object({
success: zon.boolean(),
data: zon.array(zon.object({
id: zon.number(),
title: zon.string(),
created: zon.string()
})),
error: zon.string().optional()
});
const response = await fetch('/api/data');
const zonData = await response.text();
const result = validate(zonData, ApiResponseSchema);
if (!result.success) {
throw new Error(`Invalid API response: ${result.errors.join(', ')}`);
}
Form Validation
const FormSchema = zon.object({
email: zon.string().email(),
password: zon.string().min(8),
confirmPassword: zon.string()
}).refine(data => data.password === data.confirmPassword, {
message: "Passwords don't match"
});
const result = validate(formData, FormSchema);
if (!result.success) {
displayErrors(result.errors);
}
Database Inserts
const InsertSchema = zon.object({
userId: zon.number().int(),
content: zon.string().min(1).max(1000),
tags: zon.array(zon.string()).max(5)
});
const result = validate(userInput, InsertSchema);
if (result.success) {
await db.insert('posts', result.data);
}
Error Handling
Error Format
{
success: false,
errors: [
"Invalid type: expected string, got number at path 'name'",
"Value out of range: age must be >= 0",
"Invalid enum value: role must be one of [admin, user]"
]
}
Custom Error Messages
const Schema = zon.object({
age: zon.number().min(18, "Must be 18 or older"),
email: zon.string().email("Invalid email address")
});
Advanced
Recursive Schemas
interface Category {
name: string;
children?: Category[];
}
const CategorySchema: ZonSchema = zon.object({
name: zon.string(),
children: zon.array(zon.lazy(() => CategorySchema)).optional()
});
Union Types
const IdSchema = zon.union([
zon.number(),
zon.string()
]);
validate(123, IdSchema); //
validate('abc-123', IdSchema); //
validate(true, IdSchema); //
Custom Validators
const PasswordSchema = zon.string().refine(
(val) => /^(?=.*[A-Z])(?=.*[0-9])/.test(val),
"Password must contain uppercase and number"
);
Performance
- Validation Time: ~0.01ms per field
- Memory: O(depth) stack usage
- Recommended: Validate once at API boundaries, not in hot loops
Best Practices
- Define schemas at module level - Reuse across requests
- Use
.describe()- Document fields for LLM prompts - Fail fast - Validate early in your pipeline
- Log validation errors - Help debug LLM outputs
- Don't over-constrain - LLMs struggle with complex rules
See Also
- API Reference - Full schema API
- LLM Best Practices - Using schemas with LLMs
