Skip to content
Open
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
80 changes: 80 additions & 0 deletions spec/types/literal.spec.ts
Original file line number Diff line number Diff line change
@@ -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+$` },
});
});
});
34 changes: 19 additions & 15 deletions src/transformers/literal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
};
}
}