Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/fabien0102/ts-to-zod
Generate zod schemas from typescript types/interfaces
https://github.com/fabien0102/ts-to-zod
Last synced: 5 days ago
JSON representation
Generate zod schemas from typescript types/interfaces
- Host: GitHub
- URL: https://github.com/fabien0102/ts-to-zod
- Owner: fabien0102
- License: mit
- Created: 2021-03-29T17:00:44.000Z (almost 4 years ago)
- Default Branch: main
- Last Pushed: 2024-11-29T09:13:35.000Z (about 1 month ago)
- Last Synced: 2024-12-31T08:06:44.032Z (12 days ago)
- Language: TypeScript
- Size: 655 KB
- Stars: 1,270
- Watchers: 12
- Forks: 71
- Open Issues: 50
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
- awesome - fabien0102/ts-to-zod - Generate zod schemas from typescript types/interfaces (TypeScript)
- awesome-zod - `ts-to-zod` - Convert TypeScript definitions into Zod schemas. (Convertors and Generators)
README
ts-to-zod
Generate [Zod](https://github.com/colinhacks/zod) schemas (v3) from Typescript types/interfaces.
[![Version](https://img.shields.io/npm/v/ts-to-zod.svg)](https://npmjs.org/package/ts-to-zod)
[![Github CI](https://github.com/fabien0102/ts-to-zod/actions/workflows/tests.yaml/badge.svg)](https://github.com/fabien0102/ts-to-zod/actions/workflows/tests.yaml)
[![codecov](https://codecov.io/gh/fabien0102/ts-to-zod/branch/main/graph/badge.svg?token=W5M8UIJ59C)](https://codecov.io/gh/fabien0102/ts-to-zod)
[![License](https://img.shields.io/npm/l/ts-to-zod.svg)](https://github.com/fabien0102/ts-to-zod/blob/main/LICENSE)
[![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io)## Usage
```sh
$ 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` 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 Tag Validators
This tool supports some JSDoc tags (inspired by OpenAPI) to generate additional Zod schema validators.
List of supported keywords:
| JSDoc keyword | JSDoc Example | Generated Zod validator |
| -------------------------------------------------------------------------------------------------------------------------- | -------------------------- | ------------------------------------ |
| `@minimum {number} [err_msg]` | `@minimum 42` | `z.number().min(42)` |
| `@maximum {number} [err_msg]` | `@maximum 42 Must be < 42` | `z.number().max(42, "Must be < 42")` |
| `@minLength {number} [err_msg]` | `@minLength 42` | `z.string().min(42)` |
| `@maxLength {number} [err_msg]` | `@maxLength 42` | `z.string().max(42)` |
| `@format {FormatType} [err_msg]` | `@format email` | `z.string().email()` |
| `@pattern {regex}`
**Note**: Due to parsing ambiguities, `@pattern` does _not_ support generating error messages. | `@pattern ^hello` | `z.string().regex(/^hello/)` |By default, `FormatType` is defined as the following type (corresponding Zod validator in comment):
```ts
type FormatType =
| "date-time" // z.string().datetime()
| "date" // z.string().date()
| "time" // z.string().time()
| "duration" // z.string().duration()
| "email" // z.string().email()
| "ip" // z.string().ip()
| "ipv4" // z.string().ip()
| "ipv6" // z.string().ip()
| "url" // z.string().url()
| "uuid"; // z.string().uuid()
```However, see the section on [Custom JSDoc Format Types](#custom-jsdoc-format-types) to learn more about defining other types of formats for string validation.
Those validators can be combined:
```ts
// 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 |
| ---------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| `@description {value}` | `@description Full name` | Sets the description of the property | `z.string().describe("Full name")` |
| `@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()` |
| `@schema` | `@schema .catch('foo')` | If value starts with a `.`, appends the specified value to the generated schema. Otherwise this value will override the generated schema. | `z.string().catch('foo')` |## JSDoc tag for `union` types
| JSDoc keyword | JSDoc Example | Description | Generated Zod |
| --------------------------- | ------------------- | ----------------------------------------------------- | --------------------------------- |
| `@discriminator {propName}` | `@discriminator id` | Generates a `z.discriminatedUnion()` instead of union | `z.discriminatedUnion("id", ...)` |Example:
```ts
// source.ts
/**
* @discriminator type
**/
export type Person =
| { type: "Adult"; name: string }
| { type: "Child"; age: number };// output.ts
// Generated by ts-to-zod
import { z } from "zod";export const personSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("Adult"),
name: z.string(),
}),
z.object({
type: z.literal("Child"),
age: z.number(),
}),
]);
```## JSDoc tags for elements of `string` and `number` arrays
Elements of `string` and `number` arrays can be validated using the following JSDoc tags (for details see above).
| JSDoc keyword |
| --------------------------------------- |
| `@elementDescription {value}` |
| `@elementMinimum {number} [err_msg]` |
| `@elementMaximum {number} [err_msg]` |
| `@elementMinLength {number} [err_msg]` |
| `@elementMaxLength {number} [err_msg]` |
| `@elementFormat {FormatType} [err_msg]` |
| `@elementPattern {regex}` |Example:
```ts
// source.ts
export interface EnemyContact {
/**
* The names of the enemy.
*
* @elementMinLength 5
* @elementMaxLength 10
* @minLength 2
* @maxLength 50
*/
names: string[];/**
* The phone numbers of the enemy.
*
* @description Include home and work numbers
* @elementPattern ^([+]?d{1,2}[-s]?|)d{3}[-s]?d{3}[-s]?d{4}$
*/
phoneNumbers: string[];
}// output.ts
export const enemyContactSchema = z.object({
/**
* The names of the enemy.
*
* @elementMinLength 5
* @elementMaxLength 10
* @minLength 2
* @maxLength 50
*/
name: z.array(z.string().min(5).max(10)).min(2).max(50),/**
* The phone numbers of the enemy.
*
* @elementPattern ^([+]?d{1,2}[-s]?|)d{3}[-s]?d{3}[-s]?d{4}$
*/
phoneNumbers: z
.array(z.string().regex(/^([+]?d{1,2}[-s]?|)d{3}[-s]?d{3}[-s]?d{4}$/))
.describe("Include home and work numbers"),
});
```## Advanced configuration
If you want to customize 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 type safety).
You have two ways to restrict the scope of ts-to-zod:
- `nameFilter` will filter by interface/type name
- `jsDocTagFilter` will filter on jsDocTagExample:
```ts
// 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;
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).
### Custom JSDoc Format Types
`ts-to-zod` already supports converting several `@format` types such as `email` and `ip` to built-in Zod string validation functions. However, the types supported out of the box are only a subset of those recognized by the OpenAPI specification, which doesn't fit every use case. Thus, you can use the config file to define additional format types using the `customJSDocFormatTypes` property like so:
```ts
{
"customJSDocFormatTypes": {
[formatTypeNoSpaces]:
| string
| {regex: string, errorMessage: string}
}
}
```Here is an example configuration:
```json
{
"customJSDocFormatTypes": {
"phone-number": "^\\d{3}-\\d{3}-\\d{4}$",
"date": {
"regex": "^\\d{4}-\\d{2}-\\d{2}$",
"errorMessage": "Must be in YYYY-MM-DD format."
}
}
}
```As a result, `ts-to-zod` will perform the following transformation:
TypeScript
Zod```ts
interface Info {
/**
* @format date
*/
birthdate: string;
/**
* @format phone-number Must be a valid phone number.
*/
phoneNumber: string;
}
``````ts
const infoSchema = z.object({
/**
* @format date
*/
birthdate: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/, "Must be in YYYY-MM-DD format."),
/**
* @format phone-number
*/
phoneNumber: z
.string()
.regex(/^\d{3}-\d{3}-\d{4}$/, "Must be a valid phone number."),
});
```## Limitation
Since we are generating Zod schemas, we are limited by what Zod actually supports:
- No type generics
- No `Record`
- …To resume, you can use all the primitive types and some the following typescript helpers:
- `Record`
- `Pick<>`
- `Omit<>`
- `Partial<>`
- `Required<>`
- `Array<>`
- `Promise<>`## Type references
This utility is designed to work with one file at a time (it will not split one file into several), and will handle references to types from the same file:
```ts
// 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(),
});
```### Non-Zod imports
`ts-to-zod` can reference imported types from other modules but will use an `any` validation as placeholder, as we cannot know
their schema.```ts
// source.ts
import { Person } from "@3rdparty/person";
import { Villain } from "./villain";export interface Hero {
name: string;
realPerson: Person;
nemesis: Villain;
}// output.ts
const personSchema = z.any();
const villainSchema = z.any();
export const heroSchema = z.object({
name: z.string(),
realPerson: personSchema,
nemesis: villainSchema;
});
```### Zod Imports
If an imported type is referenced in the `ts-to-zod.config.js` config (as input), this utility will automatically replace the import with the given output from the file, resolving the relative paths between both.
```ts
//ts-to-zod.config.js
/**
* ts-to-zod configuration.
*
* @type {import("./src/config").TsToZodConfig}
*/
module.exports = [
{
name: "villain",
input: "src/villain.ts",
output: "src/generated/villain.zod.ts",
getSchemaName: (id) => `z${id}`
},
{
name: "hero",
input: "src/example/heros.ts",
output: "src/example/heros.zod.ts",
},
];// heros.ts (input)
import { Person } from "@3rdparty/person";
import { Villain } from "../villain";export interface Hero {
name: string;
realPerson: Person;
nemesis: Villain;
}// heros.zod.ts (output)
import { zVillain } from "../generated/villain.zod";const personSchema = z.any();
export const heroSchema = z.object({
name: z.string(),
realPerson: personSchema,
nemesis: zVillain;
});
```## 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 a `sourceText` 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 generate `export const ${varName} = ${zodImportValue}.object(…)`
- `generateZodInferredType` help you to generate `export type ${aliasName} = ${zodImportValue}.infer`
- `generateIntegrationTests` help you to generate a file comparing the original types & zod typesTo learn more about those functions or their usages, `src/core/generate.ts` is a good starting point.
## Local development
```sh
$ git clone
$ cd ts-to-zod
$ yarn
$ yarn build
$ ./bin/run
USAGE
$ ts-to-zod [input] [output]
...
```You also have plenty of unit tests to play safely:
```sh
$ yarn test --watch
```And a playground inside `example`, buildable with the following command:
```sh
$ 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!