myzod
Schema Validation with typescript type inference.
Acknowledgements
Major Shout-out to zod for the inspiration.
Description
Myzod tries to emulate the typescript type system as much as possible and is even in some ways a little stricter. The goal is that writing a schema feels the same as defining a typescript type, with equivalent & and | operators, and well known Generic types like Record, Pick and Omit. On top of that myzod aims to offer validation within the schemas for such things as number ranges, string patterns and lengths to help enforce business logic.
The resulting package has a similar api to zod
with a little bit of inspiration from joi.
The goal is to write schemas from which the type of a successfully parsed value can be inferred. With myzod typescript types and validation logic no longer need to be maintained separately.
Performance
When parsing equivalent simple object (with nesting) schemas for myzod, zod and joi, on my machine Linux Ubuntu 18.04 running NodeJS 13.X, the results are as such:
objects parsed per second:
zod
: 51861joi
: 194325myzod
: 1288659
myzod vs zod: ~25 X Speedup
myzod vs joi: ~6 X Speedup
Installation
npm install --save myzod
Usage
Myzod is used by creating a schema, extracting the type by inferring it, and finally by parsing javascript values.
import myzod, { Infer } from 'myzod';
const personSchema = myzod.object({
id: myzod.number(),
name: myzod.string().pattern(/^[A-Z]/),
age: myzod.number().min(0),
birthdate: myzod.number().or(myzod.string()),
employed: myzod.boolean(),
friendIds: myzod.array(myzod.number()).nullable()
});
type Person = Infer<typeof personSchema>;
const person: Person = personSchema.parse({ ... });
Api Reference
Type Root
Primitive Types
Reference Types
Logical Types
myzod.Type
All myzod schemas extend the generic myzod.Type class, and as such inherit these methods:
parse
Takes an unknown value, and returns it typed if passed validation. Otherwise throws a myzod.ValidationError
parse(value: unknown): T
optional
Returns a new schema which is the union of the current schema and the UndefinedType schema.
const optionalStringSchema = myzod.string().optional();
type StringOrUndefined = Infer<typeof optionalStringSchema>;
nullable
Returns a new schema which is the union of the current schema and the NullableType schema.
const nullableStringSchema = myzod.string().nullable();
type StringOrUndefined = Infer<typeof nullableStringSchema>;
or
Shorthand for creating union types of two schemas.
const stringOrBoolSchema = myzod.string().or(myzod.boolean());
type StringOrUndefined = Infer<typeof stringOrBoolSchema>;
and
Shorthand for creating intersection types of two schemas.
const nameSchema = myzod.object({ name: myzod.string() });
const ageSchema = myzod.object({ name: myzod.number() });
const personSchema = nameSchema.and(ageSchema);
type Person = Infer<typeof personSchema>;
String
options:
- min:
number
- sets the minimum length for the string - max:
number
- sets the maximum length for the string - pattern:
RegExp
- expression string must match - predicate:
(val: string) => boolean
- predicate function to extend string validation. - predicateErrMsg:
string
- error message to throw in ValidationError should predicate fail
options can be passed as an option object or chained from schema.
myzod.string({ min: 3, max: 10, patten: /^hey/ });
myzod.string().min(3).max(10).pattern(/^hey/);
Myzod is not interested in reimplementing all possible string validations, ie isUUID, isEmail, isAlphaNumeric, etc. The myzod string validation can be easily extended using the predicate and predicateErrMsg options
const uuidSchema = myzod.string().predicate(validator.isUUID, 'expected string to be uuid');
type UUID = Infer<typeof uuidSchema>;
uuidSchema.parse('hello world');
Number
options:
- min:
number
- min value for number - max:
number
- max value for number
options can be passed as an option object or chained from schema.
myzod.number({ min: 0, max: 10 });
myzod.number().min(0).max(10);
Boolean
myzod.boolean();
Undefined
myzod.undefined();
Null
myzod.null();
Literal
Just as in typescript we can type things using literals
const schema = myzod.literal('Value');
type Val = Infer<typeof schema>;
Unknown
myzod.unknown();
The unknown schema does nothing when parsing by itself. However it is useful to require a key to be present inside an object schema when we don't know or don't care about the type.
const schema = myzod.object({ unknownYetRequiredField: myzod.unknown() });
type Schema = Infer<typeof schema>;
schema.parse({});
schema.parse({ unkownYetRequiredField: 'hello' });
Object
options:
- allowUnknown:
boolean
- allows for object with keys not specified in expected shape to succeed parsing, default false
- suppressErrPathMsg:
boolean
- suppress the path to the invalid key in thrown validationErrors. This option should stay false for most cases but is used internally to generate appropriate messages when validating nested objects. default false
myzod.object is the way to construct arbitrary object schemas.
function object(shape: { [key: string]: Type<T> }, opts?: options);
examples:
const strictEmptyObjSchema = myzod.object({});
const emptyObjSchema = myzod.object({}, { allowUnknown: true });
type Empty = Infer<typeof emptyObjSchema>;
type StrictEmpty = Infer<typeof strictEmptyObjSchema>;
emptyObjSchema.parse({ key: 'value' });
strictEmptyObjSchema.parse({ key: 'value' });
const personSchema = myzod.object({
name: myzod.string().min(2),
age: myzod.number({ min: 0 }).nullable(),
});
type Person = Infer<typeof personSchema>;
Array
options:
- length:
number
- the expected length of the array - min:
number
- the minimum length of the array - max:
number
- the maximum length of the array - unique:
boolean
- should the array be unique. default false
Signature:
function array(schema: Type<T>, opts?: Options);
Example:
const schema = myzod.array(myzod.number()).unique();
type Schema = Infer<typeof schema>;
schema.parse([1, 1, 2]);
Tuple
Tuples are similar to arrays but allow for mixed types of static length.
Note that myzod does not support intersections of tuple types at this time.
const schema = myzod.tuple([myzod.string(), myzod.object({ key: myzod.boolean() }), myzod.array(myzod.number())]);
type Schema = Infer<typeof schema>;
Record
The record type emulates as the equivalent typescript type: Record<string, T>
.
const schema = myzod.record(myzod.string());
type Schema = Infer<typeof schema>;
One primary use case of the record type is for creating schemas for objects with unknown keys that you want to have typed. This would be the equivalent of passing a pattern to joi. The way this is done in myzod is to intersect a recordSchema with a object schema.
const objSchema = myzod.object({
a: myzod.string(),
b: myzod.boolean(),
c: myzod.number(),
});
const recordSchema = myzod.record(zod.number());
const schema = objSchema.and(recordSchema);
type Schema = Infer<typeof schema>;
type Schema = {
a: string;
b: boolean;
c: number;
[key: string]: number;
};
As a utility for creating records whose values are by default optional, you can use the myzod.dictionary function.
const schema = myzod.dictionary(myzod.string());
const schema = myzod.record(myzod.string().optional());
type Schema = Infer<typeof schema>;
Enum
The enum implementation differs greatly from the original zod implementation.
In zod you would create an enum schema by passing an array of litteral schemas.
I, however, did not like this since enums are literals they must by typed out in the source code regardless, and I prefer to use actual typescript enum
values.
The cost of this approach is that I cannot statically check that you are passing an enum type to the zod.enum function. If you pass another value it won't make sense within the type system. Users beware.
enum Color {
red = 'red',
blue = 'blue',
green = 'green',
}
const colorSchema = zod.enum(Color);
Infer<typeof colorSchema>
const color = colorSchema.parse('red');
The enum schema provides a check method as a typeguard for enums.
const value: string = 'some string variable';
if (colorSchema.check(value)) {
}
Union
The myzod.union function accepts an arbitrary number of schemas and creates a union of their inferred types.
const schema = myzod.union([myzod.string(), myzod.array(myzod.string()), myzod.number()]);
type Schema = Infer<typeof schema>;
Intersection
The myzod.intersection takes two schemas as arguments and creates an intersection between their types.
const a = myzod.object({ a: myzod.string() });
const b = myzod.object({ b: myzod.string() });
const schema = myzod.intersection(a, b);
const schema = a.and(b);
const schema = b.and(a);
type Schema = Infer<typeof schema>;
Partial
The myzod.partial function takes a schema and generates a new schema equivalent to typescript's Partial type for that schema.
const personSchema = myzod.object({ name: myzod.string() });
const partialPersonSchema = myzod.partial(personSchema);
type PartialPerson = Infer<typeof partialPersonSchema>;
partialPersonSchema.parse({});
partialPersonSchema.parse({ nickName: 'lil kenny g' });
Pick
The myzod.pick function takes a myzod schema and an array of keys, and generates a new schema equivalent to typescript's Pick<T, keyof T> type.
const personSchema = myzod.object({
name: myzod.string(),
lastName: myzod.string(),
email: myzod.email(),
age: myzod.number(),
});
const nameSchema = myzod.pick(personSchema, ['name', 'lastName']);
type Named = myzod.Infer<typeof nameSchema>;
Omit
The myzod.pick function takes a myzod schema and an array of keys, and generates a new schema equivalent to typescript's Omit<T, keyof T> type.
const personSchema = myzod.object({
name: myzod.string(),
lastName: myzod.string(),
email: myzod.email(),
age: myzod.number(),
});
const nameSchema = myzod.omit(personSchema, ['email', 'age']);
type Named = myzod.Infer<typeof nameSchema>;