From 97109a92d4b0ff4d0b26a9ad7c7be6e82edef36d Mon Sep 17 00:00:00 2001 From: Oliver Zheng Date: Mon, 23 Dec 2024 14:19:37 -0800 Subject: [PATCH 1/3] Fix nullable types for OpenAPI 3.0 --- packages/zod-openapi/src/lib/zod-openapi.spec.ts | 4 +++- packages/zod-openapi/src/lib/zod-openapi.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/zod-openapi/src/lib/zod-openapi.spec.ts b/packages/zod-openapi/src/lib/zod-openapi.spec.ts index e831c43..29a15c4 100644 --- a/packages/zod-openapi/src/lib/zod-openapi.spec.ts +++ b/packages/zod-openapi/src/lib/zod-openapi.spec.ts @@ -45,6 +45,7 @@ describe('zodOpenapi', () => { aBigInt: z.bigint(), aBoolean: z.boolean(), aDate: z.date(), + aNullableString: z.string().nullable(), }), { description: `Primitives also testing overwriting of "required"`, @@ -61,8 +62,9 @@ describe('zodOpenapi', () => { aBigInt: { type: 'integer', format: 'int64' }, aBoolean: { type: 'boolean' }, aDate: { type: 'string', format: 'date-time' }, + aNullableString: { type: 'string', nullable: true }, }, - required: ['aBigInt', 'aBoolean', 'aDate', 'aNumber'], + required: ['aBigInt', 'aBoolean', 'aDate', 'aNullableString', 'aNumber'], description: 'Primitives also testing overwriting of "required"', }); }); diff --git a/packages/zod-openapi/src/lib/zod-openapi.ts b/packages/zod-openapi/src/lib/zod-openapi.ts index 1269a15..bdf67ca 100644 --- a/packages/zod-openapi/src/lib/zod-openapi.ts +++ b/packages/zod-openapi/src/lib/zod-openapi.ts @@ -359,7 +359,9 @@ function parseNullable({ const schema = generateSchema(zodRef.unwrap(), useOutput, openApiVersion); return merge( schema, - { type: typeFormat('null', openApiVersion) }, + openApiVersion === '3.0' + ? { nullable: true } + : { type: typeFormat('null', openApiVersion) }, zodRef.description ? { description: zodRef.description } : {}, ...schemas ); From b357852e3d44764bc9b37ab47954956ef8142227 Mon Sep 17 00:00:00 2001 From: Oliver Zheng Date: Mon, 23 Dec 2024 14:29:25 -0800 Subject: [PATCH 2/3] Fix when a union contains a null value for OpenAPI 3.0 --- packages/zod-openapi/src/lib/zod-openapi.spec.ts | 4 +++- packages/zod-openapi/src/lib/zod-openapi.ts | 9 ++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/zod-openapi/src/lib/zod-openapi.spec.ts b/packages/zod-openapi/src/lib/zod-openapi.spec.ts index 29a15c4..55bf965 100644 --- a/packages/zod-openapi/src/lib/zod-openapi.spec.ts +++ b/packages/zod-openapi/src/lib/zod-openapi.spec.ts @@ -46,6 +46,7 @@ describe('zodOpenapi', () => { aBoolean: z.boolean(), aDate: z.date(), aNullableString: z.string().nullable(), + aUnionIncludingNull: z.union([z.string(), z.null(), z.number()]), }), { description: `Primitives also testing overwriting of "required"`, @@ -63,8 +64,9 @@ describe('zodOpenapi', () => { aBoolean: { type: 'boolean' }, aDate: { type: 'string', format: 'date-time' }, aNullableString: { type: 'string', nullable: true }, + aUnionIncludingNull: { oneOf: [{ type: 'string' }, { type: 'number' }], nullable: true }, }, - required: ['aBigInt', 'aBoolean', 'aDate', 'aNullableString', 'aNumber'], + required: ['aBigInt', 'aBoolean', 'aDate', 'aNullableString', 'aUnionIncludingNull', 'aNumber'], description: 'Primitives also testing overwriting of "required"', }); }); diff --git a/packages/zod-openapi/src/lib/zod-openapi.ts b/packages/zod-openapi/src/lib/zod-openapi.ts index bdf67ca..8ab3229 100644 --- a/packages/zod-openapi/src/lib/zod-openapi.ts +++ b/packages/zod-openapi/src/lib/zod-openapi.ts @@ -496,10 +496,17 @@ function parseUnion({ } } + const oneOfContents = + openApiVersion === '3.0' + ? contents.filter((content) => content._def.typeName !== 'ZodNull') + : contents; + const contentsHasNull = contents.length != oneOfContents.length; + return merge( { - oneOf: contents.map((schema) => generateSchema(schema, useOutput, openApiVersion)), + oneOf: oneOfContents.map((schema) => generateSchema(schema, useOutput, openApiVersion)), }, + contentsHasNull ? { nullable: true } : {}, zodRef.description ? { description: zodRef.description } : {}, ...schemas ); From dee54692aaf8b57cf0683dfc5f6a9605d0a76261 Mon Sep 17 00:00:00 2001 From: Oliver Zheng Date: Mon, 23 Dec 2024 15:29:08 -0800 Subject: [PATCH 3/3] Fix exclusive max/min for openapi 3.0 --- .../zod-openapi/src/lib/zod-openapi.spec.ts | 4 +++ packages/zod-openapi/src/lib/zod-openapi.ts | 26 ++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/packages/zod-openapi/src/lib/zod-openapi.spec.ts b/packages/zod-openapi/src/lib/zod-openapi.spec.ts index 55bf965..09db3ba 100644 --- a/packages/zod-openapi/src/lib/zod-openapi.spec.ts +++ b/packages/zod-openapi/src/lib/zod-openapi.spec.ts @@ -47,6 +47,8 @@ describe('zodOpenapi', () => { aDate: z.date(), aNullableString: z.string().nullable(), aUnionIncludingNull: z.union([z.string(), z.null(), z.number()]), + aNumberMin: z.number().min(3).optional(), + aNumberGt: z.number().gt(5).optional(), }), { description: `Primitives also testing overwriting of "required"`, @@ -65,6 +67,8 @@ describe('zodOpenapi', () => { aDate: { type: 'string', format: 'date-time' }, aNullableString: { type: 'string', nullable: true }, aUnionIncludingNull: { oneOf: [{ type: 'string' }, { type: 'number' }], nullable: true }, + aNumberMin: { type: 'number', minimum: 3 }, + aNumberGt: { type: 'number', minimum: 5, exclusiveMinimum: true }, }, required: ['aBigInt', 'aBoolean', 'aDate', 'aNullableString', 'aUnionIncludingNull', 'aNumber'], description: 'Primitives also testing overwriting of "required"', diff --git a/packages/zod-openapi/src/lib/zod-openapi.ts b/packages/zod-openapi/src/lib/zod-openapi.ts index 8ab3229..32dc625 100644 --- a/packages/zod-openapi/src/lib/zod-openapi.ts +++ b/packages/zod-openapi/src/lib/zod-openapi.ts @@ -170,12 +170,30 @@ function parseNumber({ checks.forEach((item) => { switch (item.kind) { case 'max': - if (item.inclusive) baseSchema.maximum = item.value; - else baseSchema.exclusiveMaximum = item.value; + if (item.inclusive || openApiVersion === '3.0') { + baseSchema.maximum = item.value; + } + if (!item.inclusive) { + if (openApiVersion === '3.0') { + // exclusiveMaximum has conflicting types in oas31 and oas30 + baseSchema.exclusiveMaximum = true as unknown as number; + } else { + baseSchema.exclusiveMaximum = item.value; + } + } break; case 'min': - if (item.inclusive) baseSchema.minimum = item.value; - else baseSchema.exclusiveMinimum = item.value; + if (item.inclusive || openApiVersion === '3.0') { + baseSchema.minimum = item.value; + } + if (!item.inclusive) { + if (openApiVersion === '3.0') { + // exclusiveMinimum has conflicting types in oas31 and oas30 + baseSchema.exclusiveMinimum = true as unknown as number; + } else { + baseSchema.exclusiveMinimum = item.value; + } + } break; case 'int': baseSchema.type = typeFormat('integer', openApiVersion);