diff --git a/packages/zod-openapi/src/lib/zod-openapi.spec.ts b/packages/zod-openapi/src/lib/zod-openapi.spec.ts index e831c43..09db3ba 100644 --- a/packages/zod-openapi/src/lib/zod-openapi.spec.ts +++ b/packages/zod-openapi/src/lib/zod-openapi.spec.ts @@ -45,6 +45,10 @@ describe('zodOpenapi', () => { aBigInt: z.bigint(), aBoolean: z.boolean(), 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"`, @@ -61,8 +65,12 @@ describe('zodOpenapi', () => { aBigInt: { type: 'integer', format: 'int64' }, aBoolean: { type: 'boolean' }, 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', '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 1269a15..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); @@ -359,7 +377,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 ); @@ -494,10 +514,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 );