diff --git a/packages/zod/src/converter.test.ts b/packages/zod/src/converter.test.ts index aa93e52a3..950064907 100644 --- a/packages/zod/src/converter.test.ts +++ b/packages/zod/src/converter.test.ts @@ -149,11 +149,21 @@ const numberCases: SchemaTestCase[] = [ }, ] -enum ExampleEnum { +enum ExampleEnumString { A = 'a', B = 'b', } +enum ExampleEnumNumber { + A = 1, + B = 2, +} + +enum ExampleEnumMixed { + A = 1, + B = 'a', +} + const nativeCases: SchemaTestCase[] = [ { schema: z.boolean(), @@ -193,11 +203,19 @@ const nativeCases: SchemaTestCase[] = [ }, { schema: z.enum(['a', 'b']), - input: [true, { enum: ['a', 'b'] }], + input: [true, { type: 'string', enum: ['a', 'b'] }], + }, + { + schema: z.nativeEnum(ExampleEnumString), + input: [true, { type: 'string', enum: ['a', 'b'] }], + }, + { + schema: z.nativeEnum(ExampleEnumNumber), + input: [true, { type: 'number', enum: [1, 2] }], }, { - schema: z.nativeEnum(ExampleEnum), - input: [true, { enum: ['a', 'b'] }], + schema: z.nativeEnum(ExampleEnumMixed), + input: [true, { enum: [1, 'a'] }], }, ] diff --git a/packages/zod/src/converter.ts b/packages/zod/src/converter.ts index 7f0f5221d..34435ac6c 100644 --- a/packages/zod/src/converter.ts +++ b/packages/zod/src/converter.ts @@ -342,14 +342,25 @@ export class ZodToJsonSchemaConverter implements ConditionalSchemaConverter { case ZodFirstPartyTypeKind.ZodEnum: { const schema_ = schema as ZodEnum<[string, ...string[]]> + const values = schema_._def.values + const json: any = { enum: values, type: 'string' } - return [true, { enum: schema_._def.values }] + return [true, json] } case ZodFirstPartyTypeKind.ZodNativeEnum: { const schema_ = schema as ZodNativeEnum + const values = getEnumValues(schema_._def.values) + const json: any = { enum: values } + + if (values.every(v => typeof v === 'string')) { + json.type = 'string' + } + else if (values.every(v => Number.isFinite(v))) { + json.type = 'number' + } - return [true, { enum: Object.values(schema_._def.values) }] + return [true, json] } case ZodFirstPartyTypeKind.ZodArray: { @@ -689,3 +700,14 @@ export class ZodToJsonSchemaConverter implements ConditionalSchemaConverter { : { anyOf: [schema, { type: 'null' }] } } } + +/** + * https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/util.ts#L206C8-L212C2 + */ +function getEnumValues(entries: EnumLike) { + const numericValues = Object.values(entries).filter(v => typeof v === 'number') + const values = Object.entries(entries) + .filter(([k, _]) => !numericValues.includes(+k)) + .map(([_, v]) => v) + return values +} diff --git a/packages/zod/src/zod4/converter.native.test.ts b/packages/zod/src/zod4/converter.native.test.ts index 1133692fd..eb0553618 100644 --- a/packages/zod/src/zod4/converter.native.test.ts +++ b/packages/zod/src/zod4/converter.native.test.ts @@ -1,11 +1,21 @@ import * as z from 'zod/v4' import { testSchemaConverter } from '../../tests/shared' -enum ExampleEnum { +enum ExampleEnumString { A = 'a', B = 'b', } +enum ExampleEnumNumber { + A = 1, + B = 2, +} + +enum ExampleEnumMixed { + A = 1, + B = 'a', +} + testSchemaConverter([ { name: 'boolean', @@ -90,12 +100,22 @@ testSchemaConverter([ { name: 'enum(["a", "b"])', schema: z.enum(['a', 'b']), - input: [true, { enum: ['a', 'b'] }], + input: [true, { type: 'string', enum: ['a', 'b'] }], + }, + { + name: 'enum(ExampleEnumString)', + schema: z.enum(ExampleEnumString), + input: [true, { type: 'string', enum: ['a', 'b'] }], + }, + { + name: 'enum(ExampleEnumNumber)', + schema: z.enum(ExampleEnumNumber), + input: [true, { type: 'number', enum: [1, 2] }], }, { - name: 'enum(ExampleEnum)', - schema: z.enum(ExampleEnum), - input: [true, { enum: ['a', 'b'] }], + name: 'enum(ExampleEnumMixed)', + schema: z.enum(ExampleEnumMixed), + input: [true, { enum: [1, 'a'] }], }, { name: 'file()', diff --git a/packages/zod/src/zod4/converter.structure.test.ts b/packages/zod/src/zod4/converter.structure.test.ts index 334d55610..fe1b6e2df 100644 --- a/packages/zod/src/zod4/converter.structure.test.ts +++ b/packages/zod/src/zod4/converter.structure.test.ts @@ -43,17 +43,17 @@ testSchemaConverter([ { name: 'tuple([z.enum(["a", "b"])])', schema: z.tuple([z.enum(['a', 'b'])]), - input: [true, { type: 'array', prefixItems: [{ enum: ['a', 'b'] }] }], + input: [true, { type: 'array', prefixItems: [{ type: 'string', enum: ['a', 'b'] }] }], }, { name: 'tuple([z.enum(["a", "b"])], z.string())', schema: z.tuple([z.enum(['a', 'b'])], z.string()), - input: [true, { type: 'array', prefixItems: [{ enum: ['a', 'b'] }], items: { type: 'string' } }], + input: [true, { type: 'array', prefixItems: [{ type: 'string', enum: ['a', 'b'] }], items: { type: 'string' } }], }, { name: 'zm.tuple([zm.enum(["a", "b"])], zm.string()).check(zm.minLength(4), zm.maxLength(10))', schema: zm.tuple([zm.enum(['a', 'b'])], zm.string()).check(zm.minLength(4), zm.maxLength(10)), - input: [true, { type: 'array', prefixItems: [{ enum: ['a', 'b'] }], items: { type: 'string' }, minItems: 4, maxItems: 10 }], + input: [true, { type: 'array', prefixItems: [{ type: 'string', enum: ['a', 'b'] }], items: { type: 'string' }, minItems: 4, maxItems: 10 }], }, { name: 'set(z.string())', diff --git a/packages/zod/src/zod4/converter.ts b/packages/zod/src/zod4/converter.ts index 9347b6242..b9d1db897 100644 --- a/packages/zod/src/zod4/converter.ts +++ b/packages/zod/src/zod4/converter.ts @@ -430,7 +430,17 @@ export class ZodToJsonSchemaConverter implements ConditionalSchemaConverter { case 'enum': { const enum_ = schema as $ZodEnum - return [true, { enum: Object.values(enum_._zod.def.entries) }] + const values = getEnumValues(enum_._zod.def.entries) + const json: any = { enum: values } + + if (values.every(v => typeof v === 'string')) { + json.type = 'string' + } + else if (values.every(v => Number.isFinite(v))) { + json.type = 'number' + } + + return [true, json] } case 'literal': { @@ -637,3 +647,16 @@ export class ZodToJsonSchemaConverter implements ConditionalSchemaConverter { : undefined } } + +type EnumValue = string | number // | bigint | boolean | symbol; +type EnumLike = Readonly> +/** + * https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/util.ts#L206C8-L212C2 + */ +function getEnumValues(entries: EnumLike): EnumValue[] { + const numericValues = Object.values(entries).filter(v => typeof v === 'number') + const values = Object.entries(entries) + .filter(([k, _]) => !numericValues.includes(+k)) + .map(([_, v]) => v) + return values +}