diff --git a/spec/types/literal.spec.ts b/spec/types/literal.spec.ts new file mode 100644 index 00000000..b8400ae9 --- /dev/null +++ b/spec/types/literal.spec.ts @@ -0,0 +1,80 @@ +import { z } from 'zod'; +import { expectSchema } from '../lib/helpers'; + +describe('literal', () => { + it('generates OpenAPI schema for a string literal', () => { + expectSchema([z.literal('john').openapi('Literal')], { + Literal: { type: 'string', enum: ['john'] }, + }); + }); + + it('generates OpenAPI schema for a number literal', () => { + expectSchema([z.literal(42).openapi('Literal')], { + Literal: { type: 'number', enum: [42] }, + }); + }); + + it('generates OpenAPI schema for a boolean literal', () => { + expectSchema([z.literal(true).openapi('Literal')], { + Literal: { type: 'boolean', enum: [true] }, + }); + }); + + it('supports literals with multiple string values', () => { + expectSchema([z.literal(['john', 'jane']).openapi('Literal')], { + Literal: { type: 'string', enum: ['john', 'jane'] }, + }); + }); + + it('supports literals with multiple number values', () => { + expectSchema([z.literal([0, 1]).openapi('Literal')], { + Literal: { type: 'number', enum: [0, 1] }, + }); + }); + + it('supports literals with multiple boolean values', () => { + expectSchema([z.literal([true, false]).openapi('Literal')], { + Literal: { type: 'boolean', enum: [true, false] }, + }); + }); + + it('supports nullable literals with multiple values', () => { + expectSchema([z.literal([0, 1]).nullable().openapi('Literal')], { + Literal: { type: 'number', nullable: true, enum: [0, 1] }, + }); + }); + + it('omits the type for literals with values of different types', () => { + expectSchema([z.literal([0, 'john']).openapi('Literal')], { + Literal: { enum: [0, 'john'] }, + }); + }); + + it('supports null as one of multiple values', () => { + expectSchema([z.literal([0, null]).openapi('Literal')], { + Literal: { type: 'number', nullable: true, enum: [0, null] }, + }); + }); + + it('supports a null literal in 3.0.0', () => { + expectSchema( + [z.literal(null).openapi('Literal')], + { Literal: { nullable: true, enum: [null] } }, + { version: '3.0.0' } + ); + }); + + it('supports a null literal in 3.1.0', () => { + expectSchema( + [z.literal(null).openapi('Literal')], + { Literal: { enum: [null] } }, + { version: '3.1.0' } + ); + }); + + it('generates OpenAPI schema for a bigint literal', () => { + expectSchema([z.literal(BigInt(42)).openapi('Literal')], { + Literal: { type: 'string', pattern: `^\\d+$` }, + }); + }); +}); diff --git a/src/transformers/literal.ts b/src/transformers/literal.ts index f73b7889..1e5fc6ff 100644 --- a/src/transformers/literal.ts +++ b/src/transformers/literal.ts @@ -6,25 +6,29 @@ export class LiteralTransformer { private bigIntTransformer = new BigIntTransformer(); transform(zodSchema: ZodLiteral, mapNullableType: MapNullableType) { - const type = typeof zodSchema.def.values[0]; + // Since Zod 4 a literal can hold multiple values: z.literal([1, 2]) + const values = zodSchema.def.values; - if ( - type === 'boolean' || - type === 'number' || - type === 'string' || - type === 'object' - ) { - return { - ...mapNullableType(type), - enum: [zodSchema.def.values[0]], - }; - } + // `null` is expressed through the nullable mapping instead of a type + const types = new Set( + values.filter(value => value !== null).map(value => typeof value) + ); - if (type === 'bigint') { + if (types.has('bigint')) { return this.bigIntTransformer.transform(mapNullableType); } - // Zod doesn't really support anything else anyways - return mapNullableType('null'); + // The type is omitted when the values do not share one: z.literal([1, 'a']) + const type = types.size === 1 ? [...types][0] : undefined; + + if (type && type !== 'boolean' && type !== 'number' && type !== 'string') { + // Zod doesn't really support anything else anyways + return mapNullableType('null'); + } + + return { + ...mapNullableType(type), + enum: [...values], + }; } }