Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/openapi-ts/plugins/zod.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,34 @@ export default {

:::

## Nullish

By default, non-required object properties generate `.optional()`, which accepts `undefined` values. If you'd like non-required properties to also accept `null`, you can set `useNullish` to `true`. This generates `.nullish()` instead of `.optional()`.

::: code-group

```ts [example]
const zFoo = z.object({
bar: z.nullish(z.string()),
});
```

```js [config]
export default {
input: 'hey-api/backend', // sign up at app.heyapi.dev
output: 'src/client',
plugins: [
// ...other plugins
{
name: 'zod',
useNullish: true, // [!code ++]
},
],
};
```

:::

## Metadata

It's often useful to associate a schema with some additional [metadata](https://zod.dev/metadata) for documentation, code generation, AI structured outputs, form validation, and other purposes. If this is your use case, you can set `metadata` to `true` to generate additional metadata about schemas.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// This file is auto-generated by @hey-api/openapi-ts

import * as z from 'zod/v4-mini';

export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz');

export const zFoo = z._default(z.union([
z.object({
foo: z.nullish(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/))),
get bar() {
return z.nullish(z.lazy((): any => zBar));
},
get baz() {
return z.nullish(z.array(z.lazy((): any => zFoo)));
},
qux: z._default(z.nullish(z.int().check(z.gt(0))), 0)
}),
z.null()
]), null);

export const zBar = z.object({
foo: z.nullish(zFoo)
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// This file is auto-generated by @hey-api/openapi-ts

import { z } from 'zod';

export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz');

export const zFoo: z.ZodTypeAny = z.union([
z.object({
foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).nullish(),
bar: z.lazy(() => zBar).nullish(),
baz: z.array(z.lazy(() => zFoo)).nullish(),
qux: z.number().int().gt(0).nullish().default(0)
}),
z.null()
]).default(null);

export const zBar = z.object({
foo: zFoo.nullish()
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// This file is auto-generated by @hey-api/openapi-ts

import { z } from 'zod/v4';

export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz');

export const zFoo = z.union([
z.object({
foo: z.nullish(z.string().regex(/^\d{3}-\d{2}-\d{4}$/)),
get bar() {
return z.nullish(z.lazy((): any => zBar));
},
get baz() {
return z.nullish(z.array(z.lazy((): any => zFoo)));
},
qux: z.nullish(z.int().gt(0)).default(0)
}),
z.null()
]).default(null);

export const zBar = z.object({
foo: z.nullish(zFoo)
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// This file is auto-generated by @hey-api/openapi-ts

import * as z from 'zod/v4-mini';

export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz');

export const zQux = z.record(z.string(), z.object({
qux: z.nullish(z.string())
}));

/**
* This is Foo schema.
*/
export const zFoo = z._default(z.union([
z.object({
foo: z.nullish(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/))),
get bar() {
return z.nullish(z.lazy((): any => zBar));
},
get baz() {
return z.nullish(z.array(z.lazy((): any => zFoo)));
},
qux: z._default(z.nullish(z.int().check(z.gt(0))), 0)
}),
z.null()
]), null);

/**
* This is Bar schema.
*/
export const zBar = z.object({
foo: z.nullish(zFoo)
});

/**
* This is Foo parameter.
*/
export const zFoo2 = z.string();

export const zFoo3 = z.object({
foo: z.nullish(zBar)
});

export const zPatchFooData = z.object({
body: z.object({
foo: z.nullish(z.string())
}),
path: z.nullish(z.never()),
query: z.nullish(z.object({
foo: z.nullish(z.string()),
bar: z.nullish(zBar),
baz: z.nullish(z.object({
baz: z.nullish(z.string())
})),
qux: z.nullish(z.iso.date()),
quux: z.nullish(z.iso.datetime())
}))
});

export const zPostFooData = z.object({
body: zFoo3,
path: z.nullish(z.never()),
query: z.nullish(z.never())
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// This file is auto-generated by @hey-api/openapi-ts

import { z } from 'zod';

export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz');

export const zQux = z.record(z.object({
qux: z.string().nullish()
}));

/**
* This is Foo schema.
*/
export const zFoo: z.ZodTypeAny = z.union([
z.object({
foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).nullish(),
bar: z.lazy(() => zBar).nullish(),
baz: z.array(z.lazy(() => zFoo)).nullish(),
qux: z.number().int().gt(0).nullish().default(0)
}),
z.null()
]).default(null);

/**
* This is Bar schema.
*/
export const zBar = z.object({
foo: zFoo.nullish()
});

/**
* This is Foo parameter.
*/
export const zFoo2 = z.string();

export const zFoo3 = z.object({
foo: zBar.nullish()
});

export const zPatchFooData = z.object({
body: z.object({
foo: z.string().nullish()
}),
path: z.never().nullish(),
query: z.object({
foo: z.string().nullish(),
bar: zBar.nullish(),
baz: z.object({
baz: z.string().nullish()
}).nullish(),
qux: z.string().date().nullish(),
quux: z.string().datetime().nullish()
}).nullish()
});

export const zPostFooData = z.object({
body: zFoo3,
path: z.never().nullish(),
query: z.never().nullish()
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// This file is auto-generated by @hey-api/openapi-ts

import { z } from 'zod/v4';

export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz');

export const zQux = z.record(z.string(), z.object({
qux: z.nullish(z.string())
}));

/**
* This is Foo schema.
*/
export const zFoo = z.union([
z.object({
foo: z.nullish(z.string().regex(/^\d{3}-\d{2}-\d{4}$/)),
get bar() {
return z.nullish(z.lazy((): any => zBar));
},
get baz() {
return z.nullish(z.array(z.lazy((): any => zFoo)));
},
qux: z.nullish(z.int().gt(0)).default(0)
}),
z.null()
]).default(null);

/**
* This is Bar schema.
*/
export const zBar = z.object({
foo: z.nullish(zFoo)
});

/**
* This is Foo parameter.
*/
export const zFoo2 = z.string();

export const zFoo3 = z.object({
foo: z.nullish(zBar)
});

export const zPatchFooData = z.object({
body: z.object({
foo: z.nullish(z.string())
}),
path: z.nullish(z.never()),
query: z.nullish(z.object({
foo: z.nullish(z.string()),
bar: z.nullish(zBar),
baz: z.nullish(z.object({
baz: z.nullish(z.string())
})),
qux: z.nullish(z.iso.date()),
quux: z.nullish(z.iso.datetime())
}))
});

export const zPostFooData = z.object({
body: zFoo3,
path: z.nullish(z.never()),
query: z.nullish(z.never())
});
14 changes: 14 additions & 0 deletions packages/openapi-ts-tests/zod/v3/test/3.0.x.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ for (const zodVersion of zodVersions) {
}),
description: 'generates validator schemas',
},
{
config: createConfig({
input: 'validators.json',
output: 'validators-nullish',
plugins: [
{
compatibilityVersion: zodVersion.compatibilityVersion,
name: 'zod',
useNullish: true,
},
],
}),
description: 'generates validator schemas with nullish',
},
];

it.each(scenarios)('$description', async ({ config }) => {
Expand Down
14 changes: 14 additions & 0 deletions packages/openapi-ts-tests/zod/v3/test/3.1.x.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ for (const zodVersion of zodVersions) {
}),
description: 'generates validator schemas',
},
{
config: createConfig({
input: 'validators.yaml',
output: 'validators-nullish',
plugins: [
{
compatibilityVersion: zodVersion.compatibilityVersion,
name: 'zod',
useNullish: true,
},
],
}),
description: 'generates validator schemas with nullish',
},
{
config: createConfig({
input: 'validators.yaml',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// This file is auto-generated by @hey-api/openapi-ts

import * as z from 'zod/mini';

export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz');

export const zFoo = z._default(z.union([
z.object({
foo: z.nullish(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/))),
get bar() {
return z.nullish(z.lazy((): any => zBar));
},
get baz() {
return z.nullish(z.array(z.lazy((): any => zFoo)));
},
qux: z._default(z.nullish(z.int().check(z.gt(0))), 0)
}),
z.null()
]), null);

export const zBar = z.object({
foo: z.nullish(zFoo)
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// This file is auto-generated by @hey-api/openapi-ts

import { z } from 'zod/v3';

export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz');

export const zFoo: z.ZodTypeAny = z.union([
z.object({
foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).nullish(),
bar: z.lazy(() => zBar).nullish(),
baz: z.array(z.lazy(() => zFoo)).nullish(),
qux: z.number().int().gt(0).nullish().default(0)
}),
z.null()
]).default(null);

export const zBar = z.object({
foo: zFoo.nullish()
});
Loading
Loading