zodmut
v0.0.3
Published
Mutable Pipeline extension for zod - runtime-adaptive validation
Maintainers
Readme
zodmut
Mutable Pipeline extension for zod - runtime-adaptive validation.
Supports Zod v3 and Zod v4.
Why zodmut?
zod is great for static validation. But what if your rules need to change at runtime?
- Multi-tenant apps with tenant-specific rules
- Form builders where users define fields
- Feature flags that modify validation
- Permission-based validation rules
- Wizard / multi-step forms
zodmut adds mutable operations to zod schemas—add, remove, and update fields dynamically.
For more background on the Mutable Pipeline Pattern, see: Mutable Pipeline Pattern: Revisiting My Thoughts on Dynamic Pipelines
Installation
npm install zodmut zodQuick Start
import { z } from 'zod';
import { zodmut } from 'zodmut';
const zm = zodmut(z);
// Define schema (same as zod)
const schema = zm.object({
email: zm.string().email(),
name: zm.string().min(1),
});
// Make it mutable
const mutable = schema.mutable();
// Add fields dynamically
mutable.addField('company', zm.string().min(1));
// Remove fields
mutable.removeField('name');
// Update existing fields
mutable.updateField('email', zm.string().email().max(255));
// Check current structure
mutable.snapshot(); // ['email', 'company']
// Validate
const result = mutable.safeParse(data);
// Use with zod ecosystem (React Hook Form, tRPC, etc.)
const zodSchema = mutable.toZod();Features
Mutable Operations
const schema = zm.object({ name: zm.string() });
const mutable = schema.mutable();
mutable.addField('email', zm.string().email());
mutable.removeField('name');
mutable.updateField('email', zm.string().email().max(255));
mutable.snapshot(); // Get current field names
mutable.freeze(); // Convert back to immutableOpenAPI Support
const UserSchema = zm
.object({
id: zm.string(),
email: zm.string(),
})
.openapi('User', { description: 'A user object' })
.setFieldOpenAPI('email', { format: 'email' });
// Generate OpenAPI schema
const openAPISchema = UserSchema.getOpenAPISchema();See docs/openapi.md for details.
Protocol Buffers Support
const UserSchema = zm
.object({
id: zm.string(),
email: zm.string(),
age: zm.number(),
})
.setProtobuf({ packageName: 'user', messageName: 'User' })
.setFieldProtobuf('id', { fieldNumber: 1 })
.setFieldProtobuf('email', { fieldNumber: 2 })
.setFieldProtobuf('age', { fieldNumber: 3, type: 'int32' });
// Generate .proto definition
const proto = UserSchema.getProtobufSchema();See docs/protobuf.md for details.
External Schema Wrapping
Wrap schemas from external libraries like drizzle-zod:
import { createInsertSchema } from 'drizzle-zod';
const drizzleSchema = createInsertSchema(usersTable);
const zmSchema = zm.fromZod(drizzleSchema);
// Now you can use zodmut features
zmSchema
.mutable()
.addField('confirmEmail', zm.string())
.freeze();Ecosystem Compatibility
zodmut works with the entire zod ecosystem via toZod():
| Library | Usage |
|---------|-------|
| React Hook Form | zodResolver(schema.toZod()) |
| tRPC | .input(schema.toZod()) |
| @hono/zod-validator | zValidator('json', schema.toZod()) |
| @hono/zod-openapi | schema: schema.toZod() |
| drizzle-zod | zm.fromZod(createInsertSchema(table)) |
| zod-validation-error | fromZodError(result.error) |
See docs/integrations.md for examples.
API Reference
Initialization
import { z } from 'zod';
import { zodmut } from 'zodmut';
const zm = zodmut(z);Schema Types
zm.string()
zm.number()
zm.boolean()
zm.date()
zm.array(schema)
zm.object({ ... })
zm.enum(['a', 'b'])
zm.union([...])
zm.literal(value)
zm.tuple([...])
zm.record(keySchema, valueSchema)
zm.any()
zm.unknown()
zm.null()
zm.undefined()Object Operations
schema.extend({ newField: zm.string() })
schema.merge(otherSchema)
schema.pick(['field1', 'field2'])
schema.omit(['field1'])
schema.partial()
schema.required()
schema.passthrough()
schema.strict()
schema.strip()Validation
schema.parse(data)
schema.safeParse(data)
schema.parseAsync(data)
schema.safeParseAsync(data)Refinements
schema.refine(fn, { message: '...' })
schema.superRefine(fn)
schema.transform(fn)Metadata
// Field metadata
schema.setFieldMeta('field', { label: 'Email' })
schema.getFieldMeta('field')
// Default/catch values
schema.setFieldDefault('field', 'default')
schema.setFieldCatch('field', 'fallback')Known Limitations
Type inference in updateNested
updateNested returns MutableZodmObject<T> where T is the original type. Fields added/removed inside the callback are not reflected in TypeScript types.
const schema = zm.object({
user: zm.object({ name: zm.string() })
}).mutable();
// TypeScript doesn't know about 'age'
const updated = schema.updateNested('user', (nested) =>
nested.addField('age', zm.number())
);
// Runtime works, but no type inference for 'age'
updated.parse({ user: { name: 'John', age: 25 } });Workaround: Use explicit typing or freeze() and re-wrap if strong typing is needed.
Refinements referencing removed fields
If you use refine() or superRefine() that references a field, then remove that field, you'll get a runtime error:
const schema = zm
.object({ name: zm.string(), age: zm.number() })
.refine((data) => data.age >= 0) // references 'age'
.mutable()
.removeField('age'); // removes 'age'
schema.parse({ name: 'John' }); // Runtime error: data.age is undefinedWorkaround: Only use refinements that don't reference fields you plan to remove, or add refinements after all mutations.
Nested metadata in updateNested
When using updateNested, metadata for the parent field is preserved, but newly added nested fields don't inherit metadata automatically:
const schema = zm
.object({ user: zm.object({ name: zm.string() }) })
.setFieldOpenAPI('user', { description: 'User object' })
.mutable()
.updateNested('user', (nested) =>
nested.addField('age', zm.number()) // 'age' has no OpenAPI metadata
);Workaround: Set metadata after mutations are complete.
Why zod?
zod is an exceptional validation library. Its TypeScript-first approach, excellent type inference, and elegant API have made it the de facto standard for schema validation in the TypeScript ecosystem.
zodmut doesn't try to replace zod—it builds on top of it. All the heavy lifting (parsing, validation, error handling, type inference) is done by zod. zodmut simply adds a thin layer for runtime mutability.
If you haven't tried zod yet, you should. It's fantastic.
Acknowledgments
Built with respect and gratitude for zod by @colinhacks.
License
MIT
