Generate Zod schemas (v3) from Typescript types/interfaces.
Usage
$ yarn add --dev ts-to-zod
$ yarn ts-to-zod src/iDontTrustThisApi.ts src/nowIcanValidateEverything.ts
That's it, go to src/nowIcanValidateEverything.ts
file, you should have all the exported interface
and type
as Zod schemas with the following name pattern: ${originalType}Schema
.
Embedded validation
To make sure the generated zod schemas are 100% compatible with your original types, this tool is internally comparing z.infer<generatedSchema>
and your original type. If you are running on those validation, please open an issue 😀
Notes:
- Only exported types/interface are tested (so you can have some private types/interface and just exports the composed type)
- Even if this is not recommended, you can skip this validation step with
--skipValidation
. (At your own risk!)
JSDoc tags Validators
This tool supports some JSDoc tags inspired from openapi to generate zod validator.
List of supported keywords:
JSDoc keyword | JSDoc Example | Generated Zod validator |
---|---|---|
@minimum {number} |
@minimum 42 |
z.number().min(42) |
@maximum {number} |
@maximum 42 |
z.number().max(42) |
@minLength {number} |
@minLength 42 |
z.string().min(42) |
@maxLength {number} |
@maxLength 42 |
z.string().max(42) |
@format {"email"|"uuid"|"url"} |
@format email |
z.string().email() |
@pattern {regex} |
@pattern ^hello |
z.string().regex(/^hello/) |
Those validators can be combined:
// source.ts
export interface HeroContact {
/**
* The email of the hero.
*
* @format email
*/
email: string;
/**
* The name of the hero.
*
* @minLength 2
* @maxLength 50
*/
name: string;
/**
* The phone number of the hero.
*
* @pattern ^([+]?d{1,2}[-s]?|)d{3}[-s]?d{3}[-s]?d{4}$
*/
phoneNumber: string;
/**
* Does the hero has super power?
*
* @default true
*/
hasSuperPower?: boolean;
/**
* The age of the hero
*
* @minimum 0
* @maximum 500
*/
age: number;
}
// output.ts
export const heroContactSchema = z.object({
/**
* The email of the hero.
*
* @format email
*/
email: z.string().email(),
/**
* The name of the hero.
*
* @minLength 2
* @maxLength 50
*/
name: z.string().min(2).max(50),
/**
* The phone number of the hero.
*
* @pattern ^([+]?d{1,2}[-s]?|)d{3}[-s]?d{3}[-s]?d{4}$
*/
phoneNumber: z.string().regex(/^([+]?d{1,2}[-s]?|)d{3}[-s]?d{3}[-s]?d{4}$/),
/**
* Does the hero has super power?
*
* @default true
*/
hasSuperPower: z.boolean().default(true),
/**
* The age of the hero
*
* @minimum 0
* @maximum 500
*/
age: z.number().min(0).max(500),
});
Other JSDoc tags
Other JSDoc tags are available:
JSDoc keyword | JSDoc Example | Description | Generated Zod |
---|---|---|---|
@default {value} |
@default 42 |
Sets a default value for the property | z.number().default(42) |
@strict |
@strict |
Adds the strict() modifier to an object |
z.object().strict() |
Advanced configuration
If you want to customized the schema name or restrict the exported schemas, you can do this by adding a ts-to-zod.config.js
at the root of your project.
Just run yarn ts-to-zod --init
and you will have a ready to use configuration file (with a bit of typesafety).
You have two ways to restrict the scope of ts-to-zod:
nameFilter
will filter by interface/type namejsDocTagFilter
will filter on jsDocTag
Example:
// ts-to-zod.config.js
/**
* ts-to-zod configuration.
*
* @type {import("./src/config").TsToZodConfig}
*/
module.exports = [
{
name: "example",
input: "example/heros.ts",
output: "example/heros.zod.ts",
jsDocTagFilter: (tags) => tags.map(tag => tag.name).includes("toExtract")) // <= rule here
},
];
// example/heros.ts
/**
* Will not be part of `example/heros.zod.ts`
*/
export interface Enemy {
name: string;
powers: string[];
inPrison: boolean;
}
/**
* Will be part of `example/heros.zod.ts`
* @toExtract
*/
export interface Superman {
name: "superman" | "clark kent" | "kal-l";
enemies: Record<string, Enemy>;
age: number;
underKryptonite?: boolean;
}
/!\ Please note: if your exported interface/type have a reference to a non-exported interface/type, ts-to-zod will not be able to generate anything (missing dependencies will be reported).
Limitation
Since we are generating Zod schemas, we are limited by what Zod actually supports:
- No type generics
- No
Record<number, …>
- …
To resume, you can use all the primitive types and some the following typescript helpers:
Record<string, …>
Pick<>
Omit<>
Partial<>
Required<>
Array<>
Promise<>
This utils is design to work with one file only, and will reference types from the same file:
// source.ts
export type Id = string;
export interface Hero {
id: Id;
name: string;
}
// output.ts
export const idSchema = z.string();
export const heroSchema = z.object({
id: idSchema,
name: z.string(),
});
Programmatic API
You need more than one file? Want even more power? No problem, just use the tool as a library.
High-level function:
generate
take asourceText
and generate two file getters
Please have a look to src/core/generate.test.ts
for more examples.
Low-level functions:
generateZodSchema
help you to generateexport const ${varName} = ${zodImportValue}.object(…)
generateZodInferredType
help you to generateexport type ${aliasName} = ${zodImportValue}.infer<typeof ${zodConstName}>
generateIntegrationTests
help you to generate a file comparing the original types & zod types
To learn more about thoses functions or their usages, src/core/generate.ts
is a good starting point.
Local development
$ git clone
$ cd ts-to-zod
$ yarn
$ ./bin/run
USAGE
$ ts-to-zod [input] [output]
...
You also have plenty of unit tests to play safely:
$ yarn test --watch
And a playground inside example
, buildable with the following command:
$ yarn gen:example
Last note, if you are updating src/config.ts
, you need to run yarn gen:config
to have generate the schemas of the config (src/config.zod.ts
) (Yes, we are using the tool to build itself #inception)
Have fun!