Skip to content

Commit 480bf68

Browse files
Merge pull request #179 from kheyse-oqton/feature/zod-non-nullable-custom
[zod-openapi] a quick take on removing "nullable" and solving nullability of `z.custom()`
2 parents c3f9f7c + 777fdf6 commit 480bf68

File tree

2 files changed

+61
-17
lines changed

2 files changed

+61
-17
lines changed

packages/zod-openapi/src/lib/zod-openapi.spec.ts

+42-5
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ describe('zodOpenapi', () => {
5353
type: 'object',
5454
properties: {
5555
aUndefined: {},
56-
aNull: { type: 'string', format: 'null', nullable: true },
56+
aNull: { type: 'null' },
5757
aVoid: {},
5858
},
5959
required: ['aNull'],
@@ -239,7 +239,7 @@ describe('zodOpenapi', () => {
239239
aArrayNonempty: {
240240
type: 'array',
241241
minItems: 1,
242-
items: { type: 'string', format: 'null', nullable: true },
242+
items: { type: 'null' },
243243
},
244244
aArrayMinAndMax: {
245245
type: 'array',
@@ -624,9 +624,9 @@ describe('zodOpenapi', () => {
624624
literals: {
625625
type: 'object',
626626
properties: {
627-
wordOne: { nullable: true, type: 'string', enum: ['One'] },
627+
wordOne: { type: ['string', 'null'], enum: ['One'] },
628628
numberTwo: { type: 'number', enum: [2] },
629-
isThisTheEnd: { nullable: true, type: 'boolean', enum: [false] },
629+
isThisTheEnd: { type: ['boolean', 'null'], enum: [false] },
630630
},
631631
required: ['wordOne'],
632632
},
@@ -730,7 +730,7 @@ describe('zodOpenapi', () => {
730730
oneOf: [{ type: 'string' }, { type: 'string' }],
731731
description: 'Odd pattern here',
732732
},
733-
aNullish: { nullable: true, type: 'string' },
733+
aNullish: { type: ['string', 'null'] },
734734
stringLengthOutput: { type: 'number' },
735735
favourites: {
736736
type: 'object',
@@ -961,6 +961,42 @@ describe('zodOpenapi', () => {
961961
maximum: 10,
962962
} satisfies SchemaObject);
963963
});
964+
965+
966+
it('should work with ZodTransform and correctly set nullable and optional', () => {
967+
type Type = string;
968+
const schema = z.object({
969+
item: extendApi(
970+
z.custom<Type>((data) => true),
971+
generateSchema(z.string().nullable())
972+
),
973+
});
974+
expect(generateSchema(schema)).toEqual({
975+
properties: {
976+
item: {
977+
type: ['string', 'null'],
978+
},
979+
},
980+
type: 'object',
981+
});
982+
const schema2 = z.object({
983+
item: extendApi(
984+
z.custom<Type>((data) => !!data),
985+
generateSchema(z.string())
986+
),
987+
});
988+
expect(generateSchema(schema2)).toEqual({
989+
properties: {
990+
item: {
991+
type: 'string',
992+
},
993+
},
994+
required: ['item'],
995+
type: 'object',
996+
});
997+
998+
});
999+
9641000
test('should work with ZodReadonly', () => {
9651001
expect(generateSchema(z.object({ field: z.string() })))
9661002
.toMatchInlineSnapshot(`
@@ -991,5 +1027,6 @@ describe('zodOpenapi', () => {
9911027
"type": "object",
9921028
}
9931029
`);
1030+
9941031
});
9951032
});

packages/zod-openapi/src/lib/zod-openapi.ts

+19-12
Original file line numberDiff line numberDiff line change
@@ -299,29 +299,38 @@ function parseDate({ zodRef, schemas }: ParsingArgs<z.ZodDate>): SchemaObject {
299299
function parseNull({ zodRef, schemas }: ParsingArgs<z.ZodNull>): SchemaObject {
300300
return merge(
301301
{
302-
type: 'string' as SchemaObjectType,
303-
format: 'null',
304-
nullable: true,
302+
type: 'null' as SchemaObjectType,
305303
},
306304
zodRef.description ? { description: zodRef.description } : {},
307305
...schemas
308306
);
309307
}
310308

311-
function parseOptionalNullable({
309+
function parseOptional({
312310
schemas,
313311
zodRef,
314312
useOutput,
315-
}: ParsingArgs<
316-
z.ZodOptional<OpenApiZodAny> | z.ZodNullable<OpenApiZodAny>
317-
>): SchemaObject {
313+
}: ParsingArgs<z.ZodOptional<OpenApiZodAny>>): SchemaObject {
318314
return merge(
319315
generateSchema(zodRef.unwrap(), useOutput),
320316
zodRef.description ? { description: zodRef.description } : {},
321317
...schemas
322318
);
323319
}
324320

321+
function parseNullable({
322+
schemas,
323+
zodRef,
324+
useOutput,
325+
}: ParsingArgs<z.ZodNullable<OpenApiZodAny>>): SchemaObject {
326+
const schema = generateSchema(zodRef.unwrap(), useOutput);
327+
return merge(
328+
{ ...schema, type: [schema.type, 'null'] as SchemaObjectType[] },
329+
zodRef.description ? { description: zodRef.description } : {},
330+
...schemas
331+
);
332+
}
333+
325334
function parseDefault({
326335
schemas,
327336
zodRef,
@@ -542,8 +551,8 @@ const workerMap = {
542551
ZodBoolean: parseBoolean,
543552
ZodDate: parseDate,
544553
ZodNull: parseNull,
545-
ZodOptional: parseOptionalNullable,
546-
ZodNullable: parseOptionalNullable,
554+
ZodOptional: parseOptional,
555+
ZodNullable: parseNullable,
547556
ZodDefault: parseDefault,
548557
ZodArray: parseArray,
549558
ZodLiteral: parseLiteral,
@@ -577,11 +586,9 @@ export function generateSchema(
577586
useOutput?: boolean
578587
): SchemaObject {
579588
const { metaOpenApi = {} } = zodRef;
580-
const schemas = [
581-
zodRef.isNullable && zodRef.isNullable() ? { nullable: true } : {},
589+
const schemas: AnatineSchemaObject[] = [
582590
...(Array.isArray(metaOpenApi) ? metaOpenApi : [metaOpenApi]),
583591
];
584-
585592
try {
586593
const typeName = zodRef._def.typeName as WorkerKeys;
587594
if (typeName in workerMap) {

0 commit comments

Comments
 (0)