Skip to content

Commit cc16cbc

Browse files
Copilotmrlubos
andcommitted
Implement numeric enum support for Zod plugins (v3, v4, mini)
Co-authored-by: mrlubos <[email protected]>
1 parent a472dc8 commit cc16cbc

File tree

3 files changed

+212
-52
lines changed

3 files changed

+212
-52
lines changed

packages/openapi-ts/src/plugins/zod/mini/plugin.ts

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -195,23 +195,60 @@ const enumTypeToZodSchema = ({
195195
const result: Partial<Omit<ZodSchema, 'typeName'>> = {};
196196

197197
const enumMembers: Array<ts.LiteralExpression> = [];
198+
const literalMembers: Array<ts.CallExpression> = [];
198199

199200
let isNullable = false;
201+
let allStrings = true;
200202

201203
for (const item of schema.items ?? []) {
202-
// Zod supports only string enums
204+
// Zod supports string, number, and boolean enums
203205
if (item.type === 'string' && typeof item.const === 'string') {
204-
enumMembers.push(
205-
tsc.stringLiteral({
206-
text: item.const,
206+
const stringLiteral = tsc.stringLiteral({
207+
text: item.const,
208+
});
209+
enumMembers.push(stringLiteral);
210+
literalMembers.push(
211+
tsc.callExpression({
212+
functionName: tsc.propertyAccessExpression({
213+
expression: zSymbol.placeholder,
214+
name: identifiers.literal,
215+
}),
216+
parameters: [stringLiteral],
217+
}),
218+
);
219+
} else if (
220+
(item.type === 'number' || item.type === 'integer') &&
221+
typeof item.const === 'number'
222+
) {
223+
allStrings = false;
224+
const numberLiteral = tsc.ots.number(item.const);
225+
literalMembers.push(
226+
tsc.callExpression({
227+
functionName: tsc.propertyAccessExpression({
228+
expression: zSymbol.placeholder,
229+
name: identifiers.literal,
230+
}),
231+
parameters: [numberLiteral],
232+
}),
233+
);
234+
} else if (item.type === 'boolean' && typeof item.const === 'boolean') {
235+
allStrings = false;
236+
const booleanLiteral = tsc.ots.boolean(item.const);
237+
literalMembers.push(
238+
tsc.callExpression({
239+
functionName: tsc.propertyAccessExpression({
240+
expression: zSymbol.placeholder,
241+
name: identifiers.literal,
242+
}),
243+
parameters: [booleanLiteral],
207244
}),
208245
);
209246
} else if (item.type === 'null' || item.const === null) {
210247
isNullable = true;
211248
}
212249
}
213250

214-
if (!enumMembers.length) {
251+
if (!literalMembers.length) {
215252
return unknownTypeToZodSchema({
216253
plugin,
217254
schema: {
@@ -220,18 +257,34 @@ const enumTypeToZodSchema = ({
220257
});
221258
}
222259

223-
result.expression = tsc.callExpression({
224-
functionName: tsc.propertyAccessExpression({
225-
expression: zSymbol.placeholder,
226-
name: identifiers.enum,
227-
}),
228-
parameters: [
229-
tsc.arrayLiteralExpression({
230-
elements: enumMembers,
231-
multiLine: false,
260+
// Use z.enum() for pure string enums, z.union() for mixed or non-string types
261+
if (allStrings && enumMembers.length > 0) {
262+
result.expression = tsc.callExpression({
263+
functionName: tsc.propertyAccessExpression({
264+
expression: zSymbol.placeholder,
265+
name: identifiers.enum,
232266
}),
233-
],
234-
});
267+
parameters: [
268+
tsc.arrayLiteralExpression({
269+
elements: enumMembers,
270+
multiLine: false,
271+
}),
272+
],
273+
});
274+
} else {
275+
result.expression = tsc.callExpression({
276+
functionName: tsc.propertyAccessExpression({
277+
expression: zSymbol.placeholder,
278+
name: identifiers.union,
279+
}),
280+
parameters: [
281+
tsc.arrayLiteralExpression({
282+
elements: literalMembers,
283+
multiLine: literalMembers.length > 3,
284+
}),
285+
],
286+
});
287+
}
235288

236289
if (isNullable) {
237290
result.expression = tsc.callExpression({

packages/openapi-ts/src/plugins/zod/v3/plugin.ts

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -179,23 +179,60 @@ const enumTypeToZodSchema = ({
179179
);
180180

181181
const enumMembers: Array<ts.LiteralExpression> = [];
182+
const literalMembers: Array<ts.CallExpression> = [];
182183

183184
let isNullable = false;
185+
let allStrings = true;
184186

185187
for (const item of schema.items ?? []) {
186-
// Zod supports only string enums
188+
// Zod supports string, number, and boolean enums
187189
if (item.type === 'string' && typeof item.const === 'string') {
188-
enumMembers.push(
189-
tsc.stringLiteral({
190-
text: item.const,
190+
const stringLiteral = tsc.stringLiteral({
191+
text: item.const,
192+
});
193+
enumMembers.push(stringLiteral);
194+
literalMembers.push(
195+
tsc.callExpression({
196+
functionName: tsc.propertyAccessExpression({
197+
expression: zSymbol.placeholder,
198+
name: identifiers.literal,
199+
}),
200+
parameters: [stringLiteral],
201+
}),
202+
);
203+
} else if (
204+
(item.type === 'number' || item.type === 'integer') &&
205+
typeof item.const === 'number'
206+
) {
207+
allStrings = false;
208+
const numberLiteral = tsc.ots.number(item.const);
209+
literalMembers.push(
210+
tsc.callExpression({
211+
functionName: tsc.propertyAccessExpression({
212+
expression: zSymbol.placeholder,
213+
name: identifiers.literal,
214+
}),
215+
parameters: [numberLiteral],
216+
}),
217+
);
218+
} else if (item.type === 'boolean' && typeof item.const === 'boolean') {
219+
allStrings = false;
220+
const booleanLiteral = tsc.ots.boolean(item.const);
221+
literalMembers.push(
222+
tsc.callExpression({
223+
functionName: tsc.propertyAccessExpression({
224+
expression: zSymbol.placeholder,
225+
name: identifiers.literal,
226+
}),
227+
parameters: [booleanLiteral],
191228
}),
192229
);
193230
} else if (item.type === 'null' || item.const === null) {
194231
isNullable = true;
195232
}
196233
}
197234

198-
if (!enumMembers.length) {
235+
if (!literalMembers.length) {
199236
return unknownTypeToZodSchema({
200237
plugin,
201238
schema: {
@@ -204,18 +241,35 @@ const enumTypeToZodSchema = ({
204241
});
205242
}
206243

207-
let enumExpression = tsc.callExpression({
208-
functionName: tsc.propertyAccessExpression({
209-
expression: zSymbol.placeholder,
210-
name: identifiers.enum,
211-
}),
212-
parameters: [
213-
tsc.arrayLiteralExpression({
214-
elements: enumMembers,
215-
multiLine: false,
244+
// Use z.enum() for pure string enums, z.union() for mixed or non-string types
245+
let enumExpression: ts.CallExpression;
246+
if (allStrings && enumMembers.length > 0) {
247+
enumExpression = tsc.callExpression({
248+
functionName: tsc.propertyAccessExpression({
249+
expression: zSymbol.placeholder,
250+
name: identifiers.enum,
216251
}),
217-
],
218-
});
252+
parameters: [
253+
tsc.arrayLiteralExpression({
254+
elements: enumMembers,
255+
multiLine: false,
256+
}),
257+
],
258+
});
259+
} else {
260+
enumExpression = tsc.callExpression({
261+
functionName: tsc.propertyAccessExpression({
262+
expression: zSymbol.placeholder,
263+
name: identifiers.union,
264+
}),
265+
parameters: [
266+
tsc.arrayLiteralExpression({
267+
elements: literalMembers,
268+
multiLine: literalMembers.length > 3,
269+
}),
270+
],
271+
});
272+
}
219273

220274
if (isNullable) {
221275
enumExpression = tsc.callExpression({

packages/openapi-ts/src/plugins/zod/v4/plugin.ts

Lines changed: 73 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -172,24 +172,65 @@ const enumTypeToZodSchema = ({
172172
}): Omit<ZodSchema, 'typeName'> => {
173173
const result: Partial<Omit<ZodSchema, 'typeName'>> = {};
174174

175+
const zSymbol = plugin.referenceSymbol(
176+
plugin.api.getSelector('import', 'zod'),
177+
);
178+
175179
const enumMembers: Array<ts.LiteralExpression> = [];
180+
const literalMembers: Array<ts.CallExpression> = [];
176181

177182
let isNullable = false;
183+
let allStrings = true;
178184

179185
for (const item of schema.items ?? []) {
180-
// Zod supports only string enums
186+
// Zod supports string, number, and boolean enums
181187
if (item.type === 'string' && typeof item.const === 'string') {
182-
enumMembers.push(
183-
tsc.stringLiteral({
184-
text: item.const,
188+
const stringLiteral = tsc.stringLiteral({
189+
text: item.const,
190+
});
191+
enumMembers.push(stringLiteral);
192+
literalMembers.push(
193+
tsc.callExpression({
194+
functionName: tsc.propertyAccessExpression({
195+
expression: zSymbol.placeholder,
196+
name: identifiers.literal,
197+
}),
198+
parameters: [stringLiteral],
199+
}),
200+
);
201+
} else if (
202+
(item.type === 'number' || item.type === 'integer') &&
203+
typeof item.const === 'number'
204+
) {
205+
allStrings = false;
206+
const numberLiteral = tsc.ots.number(item.const);
207+
literalMembers.push(
208+
tsc.callExpression({
209+
functionName: tsc.propertyAccessExpression({
210+
expression: zSymbol.placeholder,
211+
name: identifiers.literal,
212+
}),
213+
parameters: [numberLiteral],
214+
}),
215+
);
216+
} else if (item.type === 'boolean' && typeof item.const === 'boolean') {
217+
allStrings = false;
218+
const booleanLiteral = tsc.ots.boolean(item.const);
219+
literalMembers.push(
220+
tsc.callExpression({
221+
functionName: tsc.propertyAccessExpression({
222+
expression: zSymbol.placeholder,
223+
name: identifiers.literal,
224+
}),
225+
parameters: [booleanLiteral],
185226
}),
186227
);
187228
} else if (item.type === 'null' || item.const === null) {
188229
isNullable = true;
189230
}
190231
}
191232

192-
if (!enumMembers.length) {
233+
if (!literalMembers.length) {
193234
return unknownTypeToZodSchema({
194235
plugin,
195236
schema: {
@@ -198,22 +239,34 @@ const enumTypeToZodSchema = ({
198239
});
199240
}
200241

201-
const zSymbol = plugin.referenceSymbol(
202-
plugin.api.getSelector('import', 'zod'),
203-
);
204-
205-
result.expression = tsc.callExpression({
206-
functionName: tsc.propertyAccessExpression({
207-
expression: zSymbol.placeholder,
208-
name: identifiers.enum,
209-
}),
210-
parameters: [
211-
tsc.arrayLiteralExpression({
212-
elements: enumMembers,
213-
multiLine: false,
242+
// Use z.enum() for pure string enums, z.union() for mixed or non-string types
243+
if (allStrings && enumMembers.length > 0) {
244+
result.expression = tsc.callExpression({
245+
functionName: tsc.propertyAccessExpression({
246+
expression: zSymbol.placeholder,
247+
name: identifiers.enum,
214248
}),
215-
],
216-
});
249+
parameters: [
250+
tsc.arrayLiteralExpression({
251+
elements: enumMembers,
252+
multiLine: false,
253+
}),
254+
],
255+
});
256+
} else {
257+
result.expression = tsc.callExpression({
258+
functionName: tsc.propertyAccessExpression({
259+
expression: zSymbol.placeholder,
260+
name: identifiers.union,
261+
}),
262+
parameters: [
263+
tsc.arrayLiteralExpression({
264+
elements: literalMembers,
265+
multiLine: literalMembers.length > 3,
266+
}),
267+
],
268+
});
269+
}
217270

218271
if (isNullable) {
219272
result.expression = tsc.callExpression({

0 commit comments

Comments
 (0)