zod-object-validator
v1.2.0
Published
Type-safe object validation with Zod - enforces validator schema to match TypeScript interface
Maintainers
Readme
zod-object-validator
Type-safe object validation with Zod. Enforces your validator schema to match your TypeScript interface at compile time.
Installation
npm install zod-object-validator zodQuick Start
import { validateObject, ValidatorSchema, coerceInt, enumType, z } from 'zod-object-validator';
// 1. Define your interface
interface UserQuery {
id: number;
name?: string;
role?: 'admin' | 'user';
}
// 2. Create a type-safe schema (TypeScript ensures all keys are defined)
const schema: ValidatorSchema<UserQuery> = {
id: coerceInt({ min: 1 }), // "5" -> 5
name: z.string().optional(), // optional string
role: enumType(['admin', 'user'] as const, { optional: true }) // optional enum
};
// 3. Validate
const result = validateObject<UserQuery>({ id: '5', name: 'John' }, schema);
// result: { id: 5, name: 'John' }The Problem
With plain Zod, there's no compile-time check that your schema matches your TypeScript interface:
interface User {
name: string;
email: string;
age?: number;
}
// No TypeScript error, but 'email' is missing!
const schema = z.object({
name: z.string(),
});The Solution
ValidatorSchema<T> enforces that every key in your interface has a corresponding validator:
import { validateObject, ValidatorSchema, z } from 'zod-object-validator';
interface User {
name: string;
email: string;
age?: number;
}
// TypeScript error: Property 'email' is missing
const badSchema: ValidatorSchema<User> = {
name: z.string(),
};
// Correct - all fields are defined
const schema: ValidatorSchema<User> = {
name: z.string().min(1),
email: z.string().email(),
age: z.number().optional(),
};
const user = validateObject<User>({ name: 'John', email: '[email protected]' }, schema);API Reference
Core
validateObject<T>(data, schema, options?): T
Validates data against a schema. Throws ZodError on failure.
const result = validateObject<User>(data, schema);Options:
| Option | Type | Description |
|--------|------|-------------|
| allowPassthrough | boolean | If true, allows fields not in schema to pass through |
// By default, extra fields are stripped
validateObject<User>({ name: 'John', extra: 'field' }, schema);
// { name: 'John' }
// With allowPassthrough, extra fields are kept
validateObject<User>({ name: 'John', extra: 'field' }, schema, { allowPassthrough: true });
// { name: 'John', extra: 'field' }validateObjectSafe<T>(data, schema, options?): ValidateObjectResult<T>
Same as validateObject but returns a result object instead of throwing.
const result = validateObjectSafe<User>(data, schema);
if (result.success) {
console.log(result.data); // User
} else {
console.log(result.error); // ZodError
}ValidatorSchema<T>
Type that enforces validators match your interface:
- Required fields:
ZodType<T[K]> - Optional fields:
ZodOptional,ZodDefault, orZodEffects
ZodError
Re-exported from Zod for error handling:
import { ZodError } from 'zod-object-validator';
try {
validateObject<User>(invalidData, schema);
} catch (error) {
if (error instanceof ZodError) {
console.log(error.errors); // Zod error array with path, message, etc.
console.log(error.message); // Formatted error message
}
}z
Re-exported from Zod for convenience:
import { z } from 'zod-object-validator';Helper Functions
All helpers accept an options object with optional and default properties.
coerceInt(options?)
Coerces strings to integers. Useful for query parameters.
| Option | Type | Description |
|--------|------|-------------|
| optional | boolean | Make field optional |
| default | number | Default value |
| min | number | Minimum value |
| max | number | Maximum value |
interface Query {
page?: number;
limit?: number;
}
const schema: ValidatorSchema<Query> = {
page: coerceInt({ optional: true, default: 1, min: 1 }),
limit: coerceInt({ optional: true, default: 20, min: 1, max: 100 }),
};
validateObject<Query>({ page: '5', limit: '50' } as any, schema);
// { page: 5, limit: 50 }coerceNumber(options?)
Coerces strings to floats. Same options as coerceInt.
coerceNumber({ min: 0, max: 100 })coerceBool(options?)
Coerces strings to booleans. Accepts: true, false, "true", "false", "1", "0".
| Option | Type | Description |
|--------|------|-------------|
| optional | boolean | Make field optional |
| default | boolean | Default value |
interface Filter {
isActive?: boolean;
}
const schema: ValidatorSchema<Filter> = {
isActive: coerceBool({ optional: true, default: true }),
};
validateObject<Filter>({ isActive: 'false' } as any, schema);
// { isActive: false }string(options?)
Enhanced string validation with trim support.
| Option | Type | Description |
|--------|------|-------------|
| optional | boolean | Make field optional |
| default | string | Default value |
| min | number | Minimum length |
| max | number | Maximum length |
| trim | boolean | Trim whitespace |
interface Input {
name: string;
bio?: string;
}
const schema: ValidatorSchema<Input> = {
name: string({ trim: true, min: 1, max: 50 }),
bio: string({ optional: true, trim: true, max: 500 }),
};numericString(options?)
Validates that a string contains only digits.
interface Input {
zipCode: string;
}
const schema: ValidatorSchema<Input> = {
zipCode: numericString(),
};enumType(values, options?)
Creates an enum validator from an array.
| Option | Type | Description |
|--------|------|-------------|
| optional | boolean | Make field optional |
| default | T | Default value |
interface Query {
sortBy?: 'name' | 'price' | 'date';
order?: 'ASC' | 'DESC';
}
const schema: ValidatorSchema<Query> = {
sortBy: enumType(['name', 'price', 'date'] as const, { optional: true }),
order: enumType(['ASC', 'DESC'] as const, { optional: true, default: 'DESC' }),
};array(itemSchema, options?)
Creates an array validator with optional constraints.
| Option | Type | Description |
|--------|------|-------------|
| optional | boolean | Make field optional |
| min | number | Minimum length |
| max | number | Maximum length |
interface Input {
tags: string[];
ids?: number[];
}
const schema: ValidatorSchema<Input> = {
tags: array(z.string(), { min: 1, max: 10 }),
ids: array(z.number(), { optional: true }),
};Real-World Examples
API Query Parameters
import { validateObject, ValidatorSchema, coerceInt, coerceBool, enumType, string } from 'zod-object-validator';
interface SearchQuery {
keyword?: string;
page?: number;
limit?: number;
sortBy?: 'name' | 'price' | 'date';
order?: 'ASC' | 'DESC';
isActive?: boolean;
}
const schema: ValidatorSchema<SearchQuery> = {
keyword: string({ optional: true, trim: true, min: 1 }),
page: coerceInt({ optional: true, default: 1, min: 1 }),
limit: coerceInt({ optional: true, default: 20, min: 1, max: 100 }),
sortBy: enumType(['name', 'price', 'date'] as const, { optional: true }),
order: enumType(['ASC', 'DESC'] as const, { optional: true, default: 'DESC' }),
isActive: coerceBool({ optional: true }),
};
// In your Express/Fastify/etc handler:
const query = validateObject<SearchQuery>(req.query as any, schema);Request Body Validation
import { validateObject, ValidatorSchema, ZodError, z } from 'zod-object-validator';
interface CreateUserBody {
name: string;
email: string;
password: string;
age?: number;
role?: 'admin' | 'user';
}
const schema: ValidatorSchema<CreateUserBody> = {
name: z.string().min(1, 'Name is required').max(100),
email: z.string().email('Invalid email format'),
password: z.string().min(8, 'Password must be at least 8 characters'),
age: z.number().int().min(0).max(150).optional(),
role: z.enum(['admin', 'user']).default('user'),
};
// In your handler:
try {
const body = validateObject<CreateUserBody>(req.body, schema);
// body is fully typed and validated
} catch (error) {
if (error instanceof ZodError) {
res.status(400).json({ errors: error.errors });
}
}Mixing Helpers with Zod
You can freely mix helper functions with native Zod schemas:
interface ProductFilter {
categoryId?: number;
priceRange?: { min: number; max: number };
tags?: string[];
}
const schema: ValidatorSchema<ProductFilter> = {
categoryId: coerceInt({ optional: true }),
priceRange: z.object({
min: z.coerce.number().min(0),
max: z.coerce.number().min(0),
}).optional(),
tags: z.union([
z.array(z.string()),
z.string().transform(s => s.split(',')),
]).optional(),
};Extending Schemas
Use spread operator to extend or compose schemas:
interface BaseUser {
name: string;
email: string;
}
const baseSchema: ValidatorSchema<BaseUser> = {
name: z.string().min(1),
email: z.string().email(),
};
interface AdminUser extends BaseUser {
role: 'admin';
permissions: string[];
}
const adminSchema: ValidatorSchema<AdminUser> = {
...baseSchema,
role: z.literal('admin'),
permissions: z.array(z.string()),
};Contributing
Contributions are welcome! See CONTRIBUTING.md for guidelines.
License
MIT © Jay Kim [email protected]
