Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/young-items-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hey-api/openapi-ts": patch
---

**plugin(valibot)**: add `enum` resolver
5 changes: 5 additions & 0 deletions .changeset/young-mitems-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hey-api/openapi-ts": patch
---

**plugin(zod)**: add `enum` resolver
52 changes: 52 additions & 0 deletions docs/openapi-ts/plugins/concepts/resolvers.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This page demonstrates resolvers through a few common scenarios.
1. [Handle arbitrary schema formats](#example-1)
2. [Validate high precision numbers](#example-2)
3. [Replace default base](#example-3)
4. [Create permissive enums](#example-4)

## Terminology

Expand Down Expand Up @@ -174,6 +175,57 @@ export const vUser = v.object({

:::

## Example 4

### Create permissive enums

By default, enum schemas are strict and will reject unknown values.

```js
export const zStatus = z.enum(['active', 'inactive', 'pending']);
```

You might want to accept unknown enum values, for example when the API adds new values that haven't been added to the spec yet. You can use the enum resolver to create a permissive union.

```js
{
name: 'zod',
'~resolvers': {
enum(ctx) {
const { $, symbols } = ctx;
const { z } = symbols;
const { allStrings, enumMembers, literalMembers } = ctx.nodes.items(ctx);

if (!allStrings || !enumMembers.length) {
return;
}

const enumSchema = $(z).attr('enum').call($.array(...enumMembers));
return $(z).attr('union').call(
$.array(enumSchema, $(z).attr('string').call())
);
}
}
}
```

This resolver creates a union that accepts both the known enum values and any other string.

::: code-group

```js [after]
export const zStatus = z.union([
z.enum(['active', 'inactive', 'pending']),
z.string(),
]);
```

```js [before]
export const zStatus = z.enum(['active', 'inactive', 'pending']);
```

:::

## Feedback

We welcome feedback on the Resolvers API. [Open a GitHub issue](https://github.com/hey-api/openapi-ts/issues) to request support for additional plugins.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// This file is auto-generated by @hey-api/openapi-ts

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

export const zFoo = z.union([z.enum(['foo', 'bar']), z.string()]);

export const zBar = z.union([z.enum(['foo', 'bar']), z.string()]);

export const zBaz = z.union([z.enum(['foo', 'bar']), z.string()]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// This file is auto-generated by @hey-api/openapi-ts

import { z } from 'zod';

export const zFoo = z.union([z.enum(['foo', 'bar']), z.string()]);

export const zBar = z.union([z.enum(['foo', 'bar']), z.string()]);

export const zBaz = z.union([z.enum(['foo', 'bar']), z.string()]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// This file is auto-generated by @hey-api/openapi-ts

import { z } from 'zod/v4';

export const zFoo = z.union([z.enum(['foo', 'bar']), z.string()]);

export const zBar = z.union([z.enum(['foo', 'bar']), z.string()]);

export const zBaz = z.union([z.enum(['foo', 'bar']), z.string()]);
31 changes: 31 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 @@ -145,6 +145,37 @@ for (const zodVersion of zodVersions) {
}),
description: 'validator schemas with string constraints union',
},
{
config: createConfig({
input: 'enum-null.json',
output: 'enum-resolver-permissive',
plugins: [
{
compatibilityVersion: zodVersion.compatibilityVersion,
name: 'zod',
'~resolvers': {
enum(ctx) {
const { $, symbols } = ctx;
const { z } = symbols;
const { allStrings, enumMembers } = ctx.nodes.items(ctx);

if (!allStrings || !enumMembers.length) {
return;
}

const enumSchema = $(z)
.attr('enum')
.call($.array(...enumMembers));
return $(z)
.attr('union')
.call($.array(enumSchema, $(z).attr('string').call()));
},
},
},
],
}),
description: 'generates permissive enums with enum resolver',
},
];

it.each(scenarios)('$description', async ({ config }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// This file is auto-generated by @hey-api/openapi-ts

import * as z from 'zod/mini';

export const zFoo = z.union([z.enum(['foo', 'bar']), z.string()]);

export const zBar = z.union([z.enum(['foo', 'bar']), z.string()]);

export const zBaz = z.union([z.enum(['foo', 'bar']), z.string()]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// This file is auto-generated by @hey-api/openapi-ts

import { z } from 'zod/v3';

export const zFoo = z.union([z.enum(['foo', 'bar']), z.string()]);

export const zBar = z.union([z.enum(['foo', 'bar']), z.string()]);

export const zBaz = z.union([z.enum(['foo', 'bar']), z.string()]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// This file is auto-generated by @hey-api/openapi-ts

import { z } from 'zod';

export const zFoo = z.union([z.enum(['foo', 'bar']), z.string()]);

export const zBar = z.union([z.enum(['foo', 'bar']), z.string()]);

export const zBaz = z.union([z.enum(['foo', 'bar']), z.string()]);
31 changes: 31 additions & 0 deletions packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,37 @@ for (const zodVersion of zodVersions) {
}),
description: 'validator schemas with string constraints union',
},
{
config: createConfig({
input: 'enum-null.json',
output: 'enum-resolver-permissive',
plugins: [
{
compatibilityVersion: zodVersion.compatibilityVersion,
name: 'zod',
'~resolvers': {
enum(ctx) {
const { $, symbols } = ctx;
const { z } = symbols;
const { allStrings, enumMembers } = ctx.nodes.items(ctx);

if (!allStrings || !enumMembers.length) {
return;
}

const enumSchema = $(z)
.attr('enum')
.call($.array(...enumMembers));
return $(z)
.attr('union')
.call($.array(enumSchema, $(z).attr('string').call()));
},
},
},
],
}),
description: 'generates permissive enums with enum resolver',
},
];

it.each(scenarios)('$description', async ({ config }) => {
Expand Down
1 change: 1 addition & 0 deletions packages/openapi-ts/src/plugins/valibot/resolvers/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type {
EnumResolverContext,
NumberResolverContext,
ObjectResolverContext,
Resolvers,
Expand Down
44 changes: 44 additions & 0 deletions packages/openapi-ts/src/plugins/valibot/resolvers/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ import type { Ast, PluginState } from '../shared/types';
import type { ValibotPlugin } from '../types';

export type Resolvers = Plugin.Resolvers<{
/**
* Resolver for enum schemas.
*
* Allows customization of how enum types are rendered.
*
* Returning `undefined` will execute the default resolver logic.
*/
enum?: (ctx: EnumResolverContext) => PipeResult | undefined;
/**
* Resolver for number schemas.
*
Expand Down Expand Up @@ -97,6 +105,42 @@ interface BaseContext extends DollarTsDsl {
};
}

export interface EnumResolverContext extends BaseContext {
/**
* Nodes used to build different parts of the enum schema.
*/
nodes: {
/**
* Returns the base enum expression (v.picklist([...])).
*/
base: (ctx: EnumResolverContext) => PipeResult;
/**
* Returns parsed enum items with metadata about the enum members.
*/
items: (ctx: EnumResolverContext) => {
/**
* String literal values for use with v.picklist([...]).
*/
enumMembers: Array<ReturnType<typeof $.literal>>;
/**
* Whether the enum includes a null value.
*/
isNullable: boolean;
};
/**
* Returns a nullable wrapper if the enum includes null, undefined otherwise.
*/
nullable: (ctx: EnumResolverContext) => PipeResult | undefined;
};
schema: SchemaWithType<'enum'>;
/**
* Utility functions for enum schema processing.
*/
utils: {
state: Refs<PluginState>;
};
}

export interface NumberResolverContext extends BaseContext {
/**
* Nodes used to build different parts of the number schema.
Expand Down
Loading
Loading