A lightweight, type-safe schema validation library for TypeScript and JavaScript with zero external dependencies.
- 🚀 Zero Dependencies: Ultra-lightweight footprint with no external runtime dependencies.
- 🔒 Type-Safe: Full static type inference with TypeScript (
Infer<typeof schema>). - 🛡️ Predictable Parsing: Support for synchronous and asynchronous safe parsing (
safeParse,safeParseAsync) and throwing parsing (parse,parseAsync). - 🧩 Rich Schema Types: Strings, numbers, booleans, objects, arrays, enums, literals, unions, records, dates, and type coercion.
- ✨ Powerful Modifiers & Pipelines:
.optional(),.nullable(),.default(),.refine(), and.transform(). - 🔍 Detailed Issue Paths & Flattening: Structured
issueswith path tracking (users[0].email) and.flatten()for form error mapping. - 📦 Dual Bundle: Ready for modern ESM (
import) and CommonJS (require), complete with TypeScript definitions.
// 1. Define a schema
const UserSchema = v.object({
id: v.number().int().min(1),
username: v.string().min(3).max(20),
email: v.email(),
isActive: v.boolean(),
tags: v.array(v.string()).min(1),
bio: v.string().optional(),
avatar: v.string().nullable(),
role: v.enum(['admin', 'member', 'guest'] as const).defaultValue('member'),
})
// 2. Infer static TypeScript type
type User = Infer<typeof UserSchema>
/*
type User = {
id: number
username: string
email: string
isActive: boolean
tags: string[]
bio?: string | undefined
avatar: string | null
role: 'admin' | 'member' | 'guest'
}
*/
// 3. Safe validation (returns result object)
const result = UserSchema.safeParse({
id: 1,
username: 'alex',
email: 'alex@example.com',
isActive: true,
tags: ['typescript', 'node'],
avatar: null,
})
if (result.success) {
console.log('Valid user:', result.data)
} else {
console.error('Validation errors:', result.errors)
console.error('Structured issues:', result.issues)
}
// 4. Or parse directly (throws VerityError on failure)
const user = UserSchema.parse({
id: 2,
username: 'sarah',
email: 'sarah@example.com',
isActive: true,
tags: ['developer'],
avatar: null,
})v.string() // StringSchema
v.number() // NumberSchema
v.boolean() // BooleanSchema
v.email() // StringSchema with email check
v.object(shape)// ObjectSchema
v.array(item) // ArraySchema
v.literal(val) // LiteralSchema
v.enum(values) // EnumSchema
v.union([...]) // UnionSchema
v.record(val) // RecordSchema
v.date() // DateSchema
v.any() // AnySchema
v.unknown() // UnknownSchema
v.coerce // Type coercion helpers (string, number, boolean, date).min(length: number, message?: string): Minimum length..max(length: number, message?: string): Maximum length..length(length: number, message?: string): Exact length..email(message?: string): RFC-compliant email validation..url(message?: string): Valid URL address (http:/https:)..uuid(message?: string): Valid UUID format..regex(pattern: RegExp, message?: string): Custom regex check..startsWith(prefix: string, message?: string): String prefix check..endsWith(suffix: string, message?: string): String suffix check..includes(substr: string, message?: string): Substring check..trim(): Trims whitespace before running validations.
const schema = v.string().trim().min(3).max(100).email('Invalid email').min(min: number, message?: string): Minimum numerical value (>= min)..max(max: number, message?: string): Maximum numerical value (<= max)..int(message?: string): Must be an integer (Number.isInteger)..positive(message?: string): Value must be> 0..nonnegative(message?: string): Value must be>= 0..negative(message?: string): Value must be< 0..nonpositive(message?: string): Value must be<= 0..finite(message?: string): Must be a finite number (Number.isFinite)..multipleOf(step: number, message?: string): Must be a multiple ofstep.
const priceSchema = v.number().positive().multipleOf(0.01)Validates boolean values (true or false). Rejects non-boolean truthy/falsy values (1, 0, 'true').
const flagSchema = v.boolean()Validates arrays and checks each element against elementSchema:
.min(length: number, message?: string): Minimum array length..max(length: number, message?: string): Maximum array length..length(length: number, message?: string): Exact array length..nonempty(message?: string): Array must have at least 1 element.
const tagsSchema = v.array(v.string().min(1)).min(1).max(5)Validates structured objects with precise nested error path tracking:
const profileSchema = v.object({
name: v.string().min(1),
age: v.number().min(18),
}).strip(): (Default) Strips unknown keys from the parsed result..passthrough(): Keeps unknown keys in the parsed result..strict(message?: string): Returns a validation error if unrecognized keys are present.
.extend(newFields): Returns a newObjectSchemamerging existing and new properties..pick(['key1', 'key2']): Returns a schema with only selected keys..omit(['key1']): Returns a schema excluding selected keys..partial(): Makes all properties optional.
const BaseUser = v.object({ id: v.number(), name: v.string() })
const AdminUser = BaseUser.extend({ permissions: v.array(v.string()) })// Literal
const roleAdmin = v.literal('admin')
// Enum
const statusSchema = v.enum(['active', 'pending', 'suspended'] as const)// Union
const idSchema = v.union([v.string(), v.number()])
// Record
const scoresSchema = v.record(v.number()) // Record<string, number>
const customKeys = v.record(v.number(), v.string().startsWith('tag_'))Validates Date instances:
.min(minDate: Date, message?: string): Date must be on or afterminDate..max(maxDate: Date, message?: string): Date must be on or beforemaxDate.
const pastDate = v.date().max(new Date())Coerces input values before validation:
v.coerce.string() // String(val)
v.coerce.number() // Number(val)
v.coerce.boolean() // Boolean(val)
v.coerce.date() // new Date(val)Every schema supports chaining:
Accepts undefined as valid data. The inferred type will have ? on object keys:
const schema = v.string().optional()Accepts null as valid data:
const schema = v.number().nullable()Fallback when input is undefined. Supports static values or factory functions:
const schema = v.string().default('anonymous')
const timestamp = v.number().default(() => Date.now())Custom validation logic (sync or async):
const Passwords = v
.object({
password: v.string().min(8),
confirm: v.string(),
})
.refine((data) => data.password === data.confirm, 'Passwords do not match')Transform data during validation:
const NumString = v.string().transform((val) => Number(val))When validation fails, VerityError provides structured information:
try {
UserSchema.parse(invalidData)
} catch (error) {
if (error instanceof VerityError) {
console.log(error.issues)
// [
// { path: ['username'], message: 'String must contain at least 3 character(s)' },
// { path: ['tags', 0], message: 'Expected string, received number' }
// ]
const { fieldErrors, formErrors } = error.flatten()
console.log(fieldErrors)
// {
// "username": ["String must contain at least 3 character(s)"],
// "tags[0]": ["Expected string, received number"]
// }
}
}Verity natively supports async validations and refinements:
const UsernameSchema = v.string().refine(async (username) => {
const available = await checkAvailability(username)
return available
}, 'Username already taken')
// Use safeParseAsync or parseAsync
const result = await UsernameSchema.safeParseAsync('alice')# Run tests
npm test
# Type check
npm run typecheck
# Lint with Biome
npm run lint
# Format code with Biome
npm run format
# Build bundle (ESM + CJS + .d.ts)
npm run build