Skip to content

Commit a89830a

Browse files
authored
Address Zod 4.3+ Override issues (#568)
1 parent 02459d2 commit a89830a

File tree

10 files changed

+153
-29
lines changed

10 files changed

+153
-29
lines changed

.changeset/fine-states-kiss.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'zod-openapi': patch
3+
---
4+
5+
Address Zod 4.3+ "cannot be represented in OpenAPI" compatibility issues

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,17 @@
2525
"type": "module",
2626
"exports": {
2727
".": {
28-
"import": "./dist/index.js",
28+
"import": "./dist/index.mjs",
2929
"require": "./dist/index.cjs"
3030
},
3131
"./api": {
32-
"import": "./dist/api.js",
32+
"import": "./dist/api.mjs",
3333
"require": "./dist/api.cjs"
3434
},
3535
"./package.json": "./package.json"
3636
},
3737
"main": "./dist/index.cjs",
38-
"module": "./dist/index.js",
38+
"module": "./dist/index.mjs",
3939
"types": "./dist/index.d.cts",
4040
"files": [
4141
"dist",
@@ -69,7 +69,7 @@
6969
"tsdown": "0.16.7",
7070
"vitest": "3.2.4",
7171
"yaml": "2.8.1",
72-
"zod": "4.1.13"
72+
"zod": "4.3.5"
7373
},
7474
"peerDependencies": {
7575
"zod": "^3.25.74 || ^4.0.0"

pnpm-lock.yaml

Lines changed: 70 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/create/schema/override.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,23 @@ import { validate } from './override.js';
55

66
describe('validate', () => {
77
it('should throw an error for a custom optional', () => {
8+
const custom = z.custom();
89
expect(() =>
910
validate(
1011
{
11-
zodSchema: z.custom().optional(),
12+
zodSchema: custom.optional(),
1213
jsonSchema: {},
1314
io: 'input',
1415
path: ['properties', 'zodOpenApiCreateSchema'],
1516
},
1617
{},
18+
{
19+
context: {
20+
jsonSchema: {},
21+
zodSchema: custom,
22+
path: ['properties', 'zodOpenApiCreateSchema'],
23+
},
24+
},
1725
),
1826
).toThrowErrorMatchingInlineSnapshot(
1927
`[Error: Zod schema of type \`custom\` at properties > zodOpenApiCreateSchema cannot be represented in OpenAPI. Please assign it metadata with \`.meta()\`]`,
@@ -30,6 +38,7 @@ describe('validate', () => {
3038
path: ['properties', 'zodOpenApiCreateSchema'],
3139
},
3240
{},
41+
{},
3342
),
3443
).toThrow(
3544
'Zod schema of type `void` at properties > zodOpenApiCreateSchema cannot be represented in OpenAPI. Please assign it metadata with `.meta()`',
@@ -46,6 +55,7 @@ describe('validate', () => {
4655
path: ['properties', 'zodOpenApiCreateSchema'],
4756
},
4857
{},
58+
{},
4959
),
5060
).toThrow(
5161
'Zod schema of type `map` at properties > zodOpenApiCreateSchema cannot be represented in OpenAPI. Please assign it metadata with `.meta()`',
@@ -65,6 +75,7 @@ describe('validate', () => {
6575
{
6676
allowEmptySchema: { pipe: { output: true } },
6777
},
78+
{},
6879
);
6980
});
7081
});

src/create/schema/override.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import type {
77
ZodOpenApiOverrideContext,
88
} from '../../types.js';
99

10+
import type { PreviousContext } from './schema.js';
11+
1012
import type { oas31 } from '@zod-openapi/openapi3-ts';
1113

1214
export const override: ZodOpenApiOverride = (ctx) => {
@@ -95,7 +97,26 @@ export const override: ZodOpenApiOverride = (ctx) => {
9597
export const validate = (
9698
ctx: ZodOpenApiOverrideContext,
9799
opts: CreateDocumentOptions,
100+
previousContext: PreviousContext,
98101
) => {
102+
if (
103+
previousContext.context &&
104+
ctx.zodSchema._zod.parent !== previousContext.context.zodSchema
105+
) {
106+
if (previousContext.context.zodSchema._zod.def.type === 'pipe') {
107+
// For some reason transform calls pipe and the meta ends up on the pipe instead of the transform
108+
throw new Error(
109+
`Zod transform found at ${previousContext.context.path.join(' > ')} are not supported in output schemas. Please use \`.overwrite()\` or wrap the schema in a \`.pipe()\` or assign it manual metadata with \`.meta()\``,
110+
);
111+
}
112+
113+
throw new Error(
114+
`Zod schema of type \`${previousContext.context.zodSchema._zod.def.type}\` at ${previousContext.context?.path.join(' > ')} cannot be represented in OpenAPI. Please assign it metadata with \`.meta()\``,
115+
);
116+
}
117+
118+
previousContext.context = undefined;
119+
99120
if (Object.keys(ctx.jsonSchema).length) {
100121
return;
101122
}
@@ -115,6 +136,7 @@ export const validate = (
115136
zodSchema: def.innerType,
116137
} as ZodOpenApiOverrideContext,
117138
opts,
139+
previousContext,
118140
);
119141
return;
120142
}
@@ -126,10 +148,14 @@ export const validate = (
126148
}
127149
case 'pipe': {
128150
if (ctx.io === 'output') {
129-
// For some reason transform calls pipe and the meta ends up on the pipe instead of the transform
130-
throw new Error(
131-
`Zod transform found at ${ctx.path.join(' > ')} are not supported in output schemas. Please use \`.overwrite()\` or wrap the schema in a \`.pipe()\` or assign it manual metadata with \`.meta()\``,
132-
);
151+
if (!ctx.zodSchema._zod.parent) {
152+
previousContext.context = ctx;
153+
return;
154+
}
155+
// // For some reason transform calls pipe and the meta ends up on the pipe instead of the transform
156+
// throw new Error(
157+
// `Zod transform found at ${ctx.path.join(' > ')} are not supported in output schemas. Please use \`.overwrite()\` or wrap the schema in a \`.pipe()\` or assign it manual metadata with \`.meta()\``,
158+
// );
133159
}
134160
return;
135161
}
@@ -147,6 +173,12 @@ export const validate = (
147173
}
148174
return;
149175
}
176+
case 'custom': {
177+
if (!ctx.zodSchema._zod.parent) {
178+
previousContext.context = ctx;
179+
return;
180+
}
181+
}
150182
}
151183

152184
throw new Error(

src/create/schema/schema.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
ZodOpenApiComponentsObject,
1515
} from '../../index.js';
1616
import { type OpenApiVersion, satisfiesVersion } from '../../openapi.js';
17+
import type { OverrideParameters } from '../../zod.js';
1718
import { type ComponentRegistry, createRegistry } from '../components.js';
1819

1920
import { override, validate } from './override.js';
@@ -26,6 +27,10 @@ export interface SchemaResult {
2627
components: Record<string, oas31.SchemaObject>;
2728
}
2829

30+
export type PreviousContext = {
31+
context?: OverrideParameters;
32+
};
33+
2934
export const createSchema = (
3035
schema: core.$ZodType,
3136
ctx: {
@@ -129,6 +134,8 @@ export const createSchemas = <
129134
? '$defs'
130135
: ('definitions' as '$defs');
131136

137+
const previousContext: PreviousContext = {};
138+
132139
const jsonSchema = toJSONSchema(zodRegistry, {
133140
override(context) {
134141
const meta = globalRegistry.get(context.zodSchema);
@@ -158,7 +165,7 @@ export const createSchemas = <
158165

159166
deleteInvalidJsonSchemaFields(context.jsonSchema);
160167
deleteZodOpenApiMeta(context.jsonSchema);
161-
validate(enrichedContext, ctx.opts);
168+
validate(enrichedContext, ctx.opts, previousContext);
162169
},
163170
io: ctx.io,
164171
unrepresentable: 'any',
@@ -175,7 +182,9 @@ export const createSchemas = <
175182

176183
const components = jsonSchema.schemas.__shared?.[defsName] ?? {};
177184

178-
jsonSchema.schemas.__shared ??= { [defsName]: components };
185+
jsonSchema.schemas.__shared ??= {
186+
[defsName]: components,
187+
} as core.ZodStandardJSONSchemaPayload<unknown>;
179188

180189
const dynamicComponents = new Map<string, string>();
181190
for (const [key, value] of Object.entries(components)) {
@@ -218,7 +227,9 @@ export const createSchemas = <
218227
) as typeof jsonSchema;
219228

220229
const parsedComponents = parsedJsonSchema.schemas.__shared?.[defsName] ?? {};
221-
parsedJsonSchema.schemas.__shared ??= { [defsName]: parsedComponents };
230+
parsedJsonSchema.schemas.__shared ??= {
231+
[defsName]: parsedComponents,
232+
} as core.ZodStandardJSONSchemaPayload<unknown>;
222233

223234
for (const [key] of ctx.registry.components.schemas.manual) {
224235
const manualComponent = parsedJsonSchema.schemas[key];

src/create/schema/tests/string.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,6 @@ describe('string', () => {
200200
${z.email()} | ${'email'}
201201
${z.iso.datetime()} | ${'date-time'}
202202
${z.iso.date()} | ${'date'}
203-
${z.iso.time()} | ${'time'}
204203
${z.iso.duration()} | ${'duration'}
205204
${z.ipv4()} | ${'ipv4'}
206205
${z.ipv6()} | ${'ipv6'}

src/types.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
/* eslint-disable @typescript-eslint/no-empty-object-type */
2-
import type { JSONSchemaMeta, toJSONSchema } from 'zod/v4/core';
2+
import type { JSONSchemaMeta } from 'zod/v4/core';
33

4-
import type { oas31 } from '@zod-openapi/openapi3-ts';
5-
6-
type OverrideParameters = Parameters<
7-
NonNullable<NonNullable<Parameters<typeof toJSONSchema>[1]>['override']>
8-
>[0];
4+
import type { OverrideParameters, OverrideSchemaParameters } from './zod.js';
95

10-
type OverrideSchemaParameters = Omit<OverrideParameters, 'zodSchema'>;
6+
import type { oas31 } from '@zod-openapi/openapi3-ts';
117

128
export type ZodOpenApiOverrideContext = OverrideParameters & {
139
io: 'input' | 'output';

src/zod.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
import type * as core from 'zod/v4/core';
2+
import type { toJSONSchema } from 'zod/v4/core';
23

34
export const isAnyZodType = (schema: unknown): schema is core.$ZodTypes =>
45
typeof schema === 'object' && schema !== null && '_zod' in schema;
6+
7+
export type OverrideParameters = Parameters<
8+
NonNullable<NonNullable<Parameters<typeof toJSONSchema>[1]>['override']>
9+
>[0];
10+
11+
export type OverrideSchemaParameters = Omit<OverrideParameters, 'zodSchema'>;

tsdown.config.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
// @ts-check
12
import { defineConfig } from 'tsdown/config';
23

34
export default defineConfig({
45
entry: ['src/index.ts', 'src/api.ts'],
5-
format: ['esm', 'cjs'],
6+
format: ['cjs', 'esm'],
67
exports: true,
78
dts: {
89
resolve: ['@zod-openapi/openapi3-ts'],

0 commit comments

Comments
 (0)