diff --git a/spec/lib/helpers.ts b/spec/lib/helpers.ts index 5e7e683..0a11df5 100644 --- a/spec/lib/helpers.ts +++ b/spec/lib/helpers.ts @@ -18,7 +18,7 @@ import { OpenApiGeneratorV31 } from '../../src/v3.1/openapi-generator'; import { OpenApiGeneratorOptions, OpenApiVersion, - SchemaRefValue, + SchemaRefs, } from '../../src/openapi-generator'; export function createSchemas( @@ -41,14 +41,17 @@ export function createSchemas( const { components } = generator.generateComponents(); - const schemaRefs: Record = (generator as any) - .generator.schemaRefs; + const schemaRefs: SchemaRefs = (generator as any).generator.schemaRefs; const schemaValues = Object.values(schemaRefs); + const pendingSchemas = schemaValues.filter( + value => Array.isArray(value) && value[0] === 'pending' + ); + // At no point should we have pending as leftover in the specs. // They are filtered when generating the final document but // in general we should never have a schema left in pending state - expect(schemaValues).not.toContain('pending'); + expect(pendingSchemas).toEqual([]); return components; } diff --git a/spec/types/recursive-schemas.spec.ts b/spec/types/recursive-schemas.spec.ts index 7348920..402792d 100644 --- a/spec/types/recursive-schemas.spec.ts +++ b/spec/types/recursive-schemas.spec.ts @@ -297,7 +297,9 @@ describe('recursive schemas (new getter approach)', () => { .object({ name: z.string(), get child() { - return recursiveSchema.optional(); + return recursiveSchema.optional().openapi({ + deprecated: true, + }); }, }) .openapi('RecursiveWithMeta', { @@ -312,7 +314,12 @@ describe('recursive schemas (new getter approach)', () => { example: { name: 'root', child: { name: 'child' } }, properties: { name: { type: 'string' }, - child: { $ref: '#/components/schemas/RecursiveWithMeta' }, + child: { + allOf: [ + { $ref: '#/components/schemas/RecursiveWithMeta' }, + { deprecated: true }, + ], + }, }, required: ['name'], }, @@ -324,10 +331,12 @@ describe('recursive schemas (new getter approach)', () => { .object({ value: z.string(), get next() { - return recursiveSchema.nullable().optional(); + return recursiveSchema + .nullable() + .openapi({ description: 'This can be null' }); }, }) - .openapi('NullableRecursive'); + .openapi('NullableRecursive', { deprecated: true }); expectSchema([recursiveSchema], { NullableRecursive: { @@ -335,11 +344,51 @@ describe('recursive schemas (new getter approach)', () => { properties: { value: { type: 'string' }, next: { - $ref: '#/components/schemas/NullableRecursive', - nullable: true, + allOf: [ + { + oneOf: [ + { $ref: '#/components/schemas/NullableRecursive' }, + { nullable: true }, + ], + }, + { description: 'This can be null' }, + ], }, }, - required: ['value'], + deprecated: true, + + required: ['value', 'next'], + }, + }); + }); + + it('supports recursive schemas with manual type passed as metadata', () => { + const recursiveSchema = z + .object({ + value: z.string(), + get next() { + return recursiveSchema.openapi({ + type: 'object', + }); + }, + }) + .openapi('RecursiveWithMetadata', { example: 3 }); + + expectSchema([recursiveSchema], { + RecursiveWithMetadata: { + type: 'object', + properties: { + value: { type: 'string' }, + next: { + allOf: [ + { $ref: '#/components/schemas/RecursiveWithMetadata' }, + { type: 'object' }, + ], + }, + }, + example: 3, + + required: ['value', 'next'], }, }); }); @@ -468,7 +517,7 @@ describe('recursive schemas (new getter approach)', () => { properties: { value: { type: 'string' }, child: { - anyOf: [ + oneOf: [ { $ref: '#/components/schemas/RecursiveNullable' }, { type: 'null' }, ], @@ -499,8 +548,10 @@ describe('recursive schemas (new getter approach)', () => { properties: { value: { type: 'string' }, child: { - $ref: '#/components/schemas/RecursiveNullable', - nullable: true, + oneOf: [ + { $ref: '#/components/schemas/RecursiveNullable' }, + { nullable: true }, + ], }, }, required: ['value'], diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index e824513..d4ce34d 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -81,10 +81,12 @@ export interface OpenApiGeneratorOptions { sortComponents?: 'alphabetically'; } -export type SchemaRefValue = SchemaObject | ReferenceObject | 'pending'; +type SchemaRefValue = SchemaObject | ReferenceObject | ['pending', ZodType]; + +export type SchemaRefs = Record; export class OpenAPIGenerator { - private schemaRefs: Record = {}; + private schemaRefs: SchemaRefs = {}; private paramRefs: Record = {}; private pathRefs: Record = {}; private rawComponents: { @@ -154,7 +156,8 @@ export class OpenAPIGenerator { private isNotPendingRefEntry( entry: [string, SchemaRefValue] ): entry is [string, SchemaObject | ReferenceObject] { - return entry[1] !== 'pending'; + const value = entry[1]; + return !Array.isArray(value) || value[0] !== 'pending'; } private get filteredSchemaRefs() { @@ -414,13 +417,17 @@ export class OpenAPIGenerator { const refId = Metadata.getRefId(zodSchema); // TODO: Do I need a similar implementation as bellow inside constructReferencedOpenAPISchema - if (refId && typeof this.schemaRefs[refId] === 'object') { + if ( + refId && + this.schemaRefs[refId] && + !Array.isArray(this.schemaRefs[refId]) + ) { return this.schemaRefs[refId]; } // If there is already a pending generation with this name // reference it directly. This means that it is recursive - if (refId && this.schemaRefs[refId] === 'pending') { + if (refId && Array.isArray(this.schemaRefs[refId])) { return { $ref: this.generateSchemaRef(refId) }; } @@ -428,7 +435,7 @@ export class OpenAPIGenerator { // any future recursive definition. It would get set to a proper // value within `generateSchemaWithRef` if (refId && !this.schemaRefs[refId]) { - this.schemaRefs[refId] = 'pending'; + this.schemaRefs[refId] = ['pending', zodSchema]; } const result = metadata?.type @@ -462,7 +469,11 @@ export class OpenAPIGenerator { const refId = Metadata.getRefId(zodSchema); // TODO: Extract this in a Recursive transformer and reuse here and within LazyTransformer - if (refId && typeof this.schemaRefs[refId] === 'object') { + if ( + refId && + this.schemaRefs[refId] && + !Array.isArray(this.schemaRefs[refId]) + ) { if ('$ref' in this.schemaRefs[refId]) { return this.schemaRefs[refId]; } @@ -482,7 +493,7 @@ export class OpenAPIGenerator { // If there is already a pending generation with this name // reference it directly. This means that it is recursive - if (refId && this.schemaRefs[refId] === 'pending') { + if (refId && Array.isArray(this.schemaRefs[refId])) { return { $ref: this.generateSchemaRef(refId) }; } @@ -490,7 +501,7 @@ export class OpenAPIGenerator { // any future recursive definition. It would get set to a proper // value within `generateSchemaWithRef` if (refId && !this.schemaRefs[refId]) { - this.schemaRefs[refId] = 'pending'; + this.schemaRefs[refId] = ['pending', zodSchema]; } return this.toOpenAPISchema(innerSchema, isNullable, defaultValue); @@ -510,16 +521,76 @@ export class OpenAPIGenerator { return this.generateSchemaWithMetadata(zodSchema); } - const schemaRef = this.schemaRefs[refId] as SchemaObject; const referenceObject: ReferenceObject = { $ref: this.generateSchemaRef(refId), }; // We are currently calculating this schema or there is nothing - if (this.schemaRefs[refId] === 'pending') { - return referenceObject; + if (Array.isArray(this.schemaRefs[refId])) { + const schemaAtDefinition = this.schemaRefs[refId][1]; + + const metadataAtDefinition = + Metadata.getOpenApiMetadata(schemaAtDefinition); + + const schemaRef = metadataAtDefinition as SchemaObject; + // Metadata provided from .openapi() that is new to what we had already registered + const newMetadata = omitBy( + Metadata.buildSchemaMetadata(metadata ?? {}), + (value, key) => + value === undefined || objectEquals(value, schemaRef[key]) + ); + + // Do not calculate schema metadata overrides if type is provided in .openapi + // https://github.com/asteasolutions/zod-to-openapi/pull/52/files/8ff707fe06e222bc573ed46cf654af8ee0b0786d#r996430801 + if (newMetadata.type) { + return { + allOf: [referenceObject, newMetadata], + }; + } + + // TODO: Not sure if we need to bring this back + + // New metadata from zodSchema properties. + // const newSchemaMetadata = omitBy( + // this.openApiTransformer.toNullableType( + // zodSchema, + // isNullableSchema(zodSchema) + // ), + // (value, key) => + // value === undefined || + // objectEquals( + // value, + // this.openApiTransformer.toNullableType( + // schemaAtDefinition, + // isNullableSchema(schemaAtDefinition) + // )[key] + // ) + // ); + + let typeSchema: SchemaObject | ReferenceObject = referenceObject; + if ( + isNullableSchema(zodSchema) && + !isNullableSchema(schemaAtDefinition) + ) { + typeSchema = { + oneOf: this.versionSpecifics.mapNullableOfArray( + [referenceObject], + true + ), + }; + } + + if (Object.keys(newMetadata).length > 0) { + return { + allOf: [typeSchema, newMetadata], + }; + } + + return typeSchema; } + const schemaRef = this.schemaRefs[refId] as SchemaObject; + // Metadata provided from .openapi() that is new to what we had already registered const newMetadata = omitBy( Metadata.buildSchemaMetadata(metadata ?? {}), diff --git a/src/transformers/array.ts b/src/transformers/array.ts index 5568477..3de1502 100644 --- a/src/transformers/array.ts +++ b/src/transformers/array.ts @@ -3,6 +3,10 @@ import { MapNullableType, MapSubSchema } from '../types'; import { $ZodCheckMinLength, $ZodCheckMaxLength } from 'zod/core'; import { isAnyZodType } from '../lib/zod-is-type'; export class ArrayTransformer { + get openApiType() { + return 'array' as const; + } + transform( zodSchema: ZodArray, mapNullableType: MapNullableType, @@ -21,7 +25,7 @@ export class ArrayTransformer { )?._zod.def.maximum; return { - ...mapNullableType('array'), + ...mapNullableType(this.openApiType), items: isAnyZodType(itemType) ? mapItems(itemType) : {}, minItems, diff --git a/src/transformers/big-int.ts b/src/transformers/big-int.ts index 9e5daef..6e2f7c3 100644 --- a/src/transformers/big-int.ts +++ b/src/transformers/big-int.ts @@ -1,9 +1,13 @@ import { MapNullableType } from '../types'; export class BigIntTransformer { + get openApiType() { + return 'string' as const; + } + transform(mapNullableType: MapNullableType) { return { - ...mapNullableType('string'), + ...mapNullableType(this.openApiType), pattern: `^\d+$`, }; } diff --git a/src/transformers/date.ts b/src/transformers/date.ts index c62313d..8418f4b 100644 --- a/src/transformers/date.ts +++ b/src/transformers/date.ts @@ -1,9 +1,13 @@ import { MapNullableType } from '../types'; export class DateTransformer { + get openApiType() { + return 'string' as const; + } + transform(mapNullableType: MapNullableType) { return { - ...mapNullableType('string'), + ...mapNullableType(this.openApiType), format: 'date', }; } diff --git a/src/transformers/discriminated-union.ts b/src/transformers/discriminated-union.ts index c7d16ed..e44cc05 100644 --- a/src/transformers/discriminated-union.ts +++ b/src/transformers/discriminated-union.ts @@ -3,12 +3,25 @@ import { DiscriminatorObject, MapNullableOfArrayWithNullable, MapSubSchema, + SchemaObject, } from '../types'; import { isString } from '../lib/lodash'; import { isZodType } from '../lib/zod-is-type'; import { Metadata } from '../metadata'; export class DiscriminatedUnionTransformer { + openApiType(zodSchema: ZodDiscriminatedUnion, mapToType: MapSubSchema) { + const options = [...zodSchema.def.options] as ZodObject[]; + + const optionSchema = options.map(mapToType); + + const oneOfSchema: SchemaObject = { + oneOf: optionSchema, + }; + + return oneOfSchema; + } + transform( zodSchema: ZodDiscriminatedUnion, isNullable: boolean, diff --git a/src/transformers/index.ts b/src/transformers/index.ts index 26a7863..b388d4c 100644 --- a/src/transformers/index.ts +++ b/src/transformers/index.ts @@ -80,6 +80,139 @@ export class OpenApiTransformer { return { ...schema, default: defaultValue }; } + toNullableType(zodSchema: ZodType, isNullable: boolean) { + const innerSchema = Metadata.unwrapChained(zodSchema); + return this.toNullableTypeInner(innerSchema, isNullable); + } + + private toNullableTypeInner(zodSchema: ZodType, isNullable: boolean) { + if (isZodType(zodSchema, 'ZodNull')) { + return this.versionSpecifics.nullType; + } + + if (isZodType(zodSchema, 'ZodUnknown') || isZodType(zodSchema, 'ZodAny')) { + return this.versionSpecifics.mapNullableType(undefined, isNullable); + } + + if (isZodType(zodSchema, 'ZodObject')) { + return this.versionSpecifics.mapNullableType( + this.objectTransformer.openApiType, + isNullable + ); + } + + if (isZodType(zodSchema, 'ZodString')) { + return this.versionSpecifics.mapNullableType( + this.stringTransformer.openApiType, + isNullable + ); + } + + if (isZodType(zodSchema, 'ZodNumber')) { + return this.versionSpecifics.mapNullableType( + this.numberTransformer.openApiType, + isNullable + ); + } + + if (isZodType(zodSchema, 'ZodBigInt')) { + return this.versionSpecifics.mapNullableType( + this.bigIntTransformer.openApiType, + isNullable + ); + } + + if (isZodType(zodSchema, 'ZodBoolean')) { + return this.versionSpecifics.mapNullableType('boolean', isNullable); + } + + // if (isZodType(zodSchema, 'ZodLazy')) { + // return this.lazyTransformer.transform(zodSchema, mapItem, schema => + // this.versionSpecifics.mapNullableType(schema, true) + // ); + // } + + if (isZodType(zodSchema, 'ZodLiteral')) { + const { type } = this.literalTransformer.transform(zodSchema, schema => + this.versionSpecifics.mapNullableType(schema, isNullable) + ); + + return this.versionSpecifics.mapNullableType(type, isNullable); + } + + if (isZodType(zodSchema, 'ZodEnum')) { + const { type } = this.enumTransformer.transform(zodSchema, schema => + this.versionSpecifics.mapNullableType(schema, isNullable) + ); + + return this.versionSpecifics.mapNullableType(type, isNullable); + } + + if (isZodType(zodSchema, 'ZodArray')) { + return this.versionSpecifics.mapNullableType( + this.arrayTransformer.openApiType, + isNullable + ); + } + + if (isZodType(zodSchema, 'ZodTuple')) { + return this.versionSpecifics.mapNullableType( + this.tupleTransformer.openApiType, + isNullable + ); + } + + // Note: It is important that this goes above the union transformer + // because the discriminated union is still a union + // if (isZodType(zodSchema, 'ZodDiscriminatedUnion')) { + // return this.discriminatedUnionTransformer.transform( + // zodSchema, + // true, + // _ => this.versionSpecifics.mapNullableOfArray(_, true), + // mapItem, + // generateSchemaRef + // ); + // } + + if (isZodType(zodSchema, 'ZodUnion')) { + const baseSchema = this.unionTransformer.openApiType(zodSchema, schema => + this.toNullableType(schema, isNullable) + ); + + return this.versionSpecifics.mapNullableOfArray([baseSchema], true); + } + + if (isZodType(zodSchema, 'ZodIntersection')) { + const allOfSchema = this.intersectionTransformer.openApiType( + zodSchema, + schema => this.toNullableType(schema, isNullable) + ); + + return this.versionSpecifics.mapNullableOfArray([allOfSchema], true); + } + + if (isZodType(zodSchema, 'ZodRecord')) { + return this.versionSpecifics.mapNullableType( + this.recordTransformer.openApiType, + isNullable + ); + } + + if (isZodType(zodSchema, 'ZodDate')) { + return this.versionSpecifics.mapNullableType( + this.dateTransformer.openApiType, + isNullable + ); + } + + const refId = Metadata.getRefId(zodSchema); + + throw new UnknownZodTypeError({ + currentSchema: zodSchema.def, + schemaName: refId, + }); + } + private transformSchemaWithoutDefault( zodSchema: ZodType, isNullable: boolean, @@ -163,7 +296,8 @@ export class OpenApiTransformer { if (isZodType(zodSchema, 'ZodUnion')) { return this.unionTransformer.transform( zodSchema, - _ => this.versionSpecifics.mapNullableOfArray(_, isNullable), + isNullable, + (...args) => this.versionSpecifics.mapNullableOfArray(...args), mapItem ); } @@ -172,7 +306,7 @@ export class OpenApiTransformer { return this.intersectionTransformer.transform( zodSchema, isNullable, - _ => this.versionSpecifics.mapNullableOfArray(_, isNullable), + (...args) => this.versionSpecifics.mapNullableOfArray(...args), mapItem ); } @@ -180,7 +314,8 @@ export class OpenApiTransformer { if (isZodType(zodSchema, 'ZodRecord')) { return this.recordTransformer.transform( zodSchema, - _ => this.versionSpecifics.mapNullableType(_, isNullable), + isNullable, + (...args) => this.versionSpecifics.mapNullableType(...args), mapItem ); } diff --git a/src/transformers/intersection.ts b/src/transformers/intersection.ts index b9f6fc5..8e0d143 100644 --- a/src/transformers/intersection.ts +++ b/src/transformers/intersection.ts @@ -7,17 +7,23 @@ import { ZodIntersection, ZodType } from 'zod'; import { isAnyZodType, isZodType } from '../lib/zod-is-type'; export class IntersectionTransformer { + openApiType(zodSchema: ZodIntersection, mapToType: MapSubSchema) { + const subtypes = this.flattenIntersectionTypes(zodSchema); + + const allOfSchema: SchemaObject = { + allOf: subtypes.map(mapToType), + }; + + return allOfSchema; + } + transform( zodSchema: ZodIntersection, isNullable: boolean, mapNullableOfArray: MapNullableOfArrayWithNullable, mapItem: MapSubSchema ): SchemaObject { - const subtypes = this.flattenIntersectionTypes(zodSchema); - - const allOfSchema: SchemaObject = { - allOf: subtypes.map(mapItem), - }; + const allOfSchema = this.openApiType(zodSchema, mapItem); if (isNullable) { return { diff --git a/src/transformers/number.ts b/src/transformers/number.ts index 0bd050a..bb48dba 100644 --- a/src/transformers/number.ts +++ b/src/transformers/number.ts @@ -2,13 +2,17 @@ import { ZodNumber } from 'zod'; import { MapNullableType, GetNumberChecks } from '../types'; export class NumberTransformer { + get openApiType() { + return 'number' as const; + } + transform( zodSchema: ZodNumber, mapNullableType: MapNullableType, getNumberChecks: GetNumberChecks ) { return { - ...mapNullableType('number'), + ...mapNullableType(this.openApiType), ...mapNullableType(zodSchema.format === 'safeint' ? 'integer' : 'number'), ...getNumberChecks(zodSchema.def.checks ?? []), }; diff --git a/src/transformers/object.ts b/src/transformers/object.ts index c5c180d..02a9b98 100644 --- a/src/transformers/object.ts +++ b/src/transformers/object.ts @@ -5,6 +5,10 @@ import { mapValues, objectEquals } from '../lib/lodash'; import { Metadata } from '../metadata'; export class ObjectTransformer { + get openApiType() { + return 'object' as const; + } + transform( zodSchema: ZodObject, defaultValue: object, @@ -18,7 +22,7 @@ export class ObjectTransformer { if (!extendedFrom) { return { - ...mapNullableType('object'), + ...mapNullableType(this.openApiType), properties, default: defaultValue, diff --git a/src/transformers/record.ts b/src/transformers/record.ts index 69d7924..7025bfa 100644 --- a/src/transformers/record.ts +++ b/src/transformers/record.ts @@ -1,12 +1,21 @@ -import { MapNullableType, MapSubSchema, SchemaObject } from '../types'; +import { + MapNullableTypeWithNullable, + MapSubSchema, + SchemaObject, +} from '../types'; import { ZodRecord } from 'zod'; import { isAnyZodType, isZodType } from '../lib/zod-is-type'; import { isString } from '../lib/lodash'; export class RecordTransformer { + get openApiType() { + return 'object' as const; + } + transform( zodSchema: ZodRecord, - mapNullableType: MapNullableType, + isNullable: boolean, + mapNullableType: MapNullableTypeWithNullable, mapItem: MapSubSchema ): SchemaObject { const propertiesType = zodSchema.valueType; @@ -30,13 +39,13 @@ export class RecordTransformer { ); return { - ...mapNullableType('object'), + ...mapNullableType(this.openApiType, isNullable), properties, }; } return { - ...mapNullableType('object'), + ...mapNullableType(this.openApiType, isNullable), additionalProperties: propertiesSchema, }; } diff --git a/src/transformers/string.ts b/src/transformers/string.ts index 4c0b171..63056af 100644 --- a/src/transformers/string.ts +++ b/src/transformers/string.ts @@ -21,6 +21,10 @@ function isZodCheckRegex(check: $ZodCheck): check is $ZodCheckRegex { } export class StringTransformer { + get openApiType() { + return 'string' as const; + } + transform(zodSchema: ZodString, mapNullableType: MapNullableType) { const regexCheck = zodSchema.def.checks?.find(isZodCheckRegex); // toString generates an additional / at the beginning and end of the pattern @@ -40,7 +44,7 @@ export class StringTransformer { : undefined; return { - ...mapNullableType('string'), + ...mapNullableType(this.openApiType), // FIXME: https://github.com/colinhacks/zod/commit/d78047e9f44596a96d637abb0ce209cd2732d88c minLength: length ?? maxLength, maxLength: length ?? minLength, diff --git a/src/transformers/tuple.ts b/src/transformers/tuple.ts index 981c69a..4dc413a 100644 --- a/src/transformers/tuple.ts +++ b/src/transformers/tuple.ts @@ -6,6 +6,10 @@ import { isAnyZodType } from '../lib/zod-is-type'; export class TupleTransformer { constructor(private versionSpecifics: OpenApiVersionSpecifics) {} + get openApiType() { + return 'array' as const; + } + transform( zodSchema: ZodTuple, mapNullableType: MapNullableType, @@ -18,7 +22,7 @@ export class TupleTransformer { ); return { - ...mapNullableType('array'), + ...mapNullableType(this.openApiType), ...this.versionSpecifics.mapTupleItems(schemas), }; } diff --git a/src/transformers/union.ts b/src/transformers/union.ts index 7ff8025..067d507 100644 --- a/src/transformers/union.ts +++ b/src/transformers/union.ts @@ -1,5 +1,9 @@ import { ZodType, ZodUnion } from 'zod'; -import { MapNullableOfArray, MapSubSchema } from '../types'; +import { + MapNullableOfArrayWithNullable, + MapSubSchema, + SchemaObject, +} from '../types'; import { isAnyZodType, isZodType } from '../lib/zod-is-type'; import { Metadata } from '../metadata'; import { UnionPreferredType } from '../zod-extensions'; @@ -7,9 +11,37 @@ import { UnionPreferredType } from '../zod-extensions'; export class UnionTransformer { constructor(private options?: { unionPreferredType?: UnionPreferredType }) {} + openApiType(zodSchema: ZodUnion, mapToType: MapSubSchema) { + const internalMetadata = Metadata.getInternalMetadata(zodSchema); + + const preferredType = + internalMetadata?.unionPreferredType ?? + this.options?.unionPreferredType ?? + 'anyOf'; + + const options = this.flattenUnionTypes(zodSchema); + + const schemas = options.map(schema => { + // If any of the underlying schemas of a union is .nullable then the whole union + // would be nullable. `mapNullableOfArray` would place it where it belongs. + // Therefor we are stripping the additional nullables from the inner schemas + // See https://github.com/asteasolutions/zod-to-openapi/issues/149 + const optionToGenerate = this.unwrapNullable(schema); + + return mapToType(optionToGenerate); + }); + + const preferredTypeSchema: SchemaObject = { + [preferredType]: schemas, + }; + + return preferredTypeSchema; + } + transform( zodSchema: ZodUnion, - mapNullableOfArray: MapNullableOfArray, + isNullable: boolean, + mapNullableOfArray: MapNullableOfArrayWithNullable, mapItem: MapSubSchema ) { const internalMetadata = Metadata.getInternalMetadata(zodSchema); @@ -32,7 +64,7 @@ export class UnionTransformer { }); return { - [preferredType]: mapNullableOfArray(schemas), + [preferredType]: mapNullableOfArray(schemas, isNullable), }; } diff --git a/src/types.ts b/src/types.ts index 7b1e151..d4d564b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -56,10 +56,6 @@ export type MapNullableTypeWithNullable = ( isNullable: boolean ) => Pick; -export type MapNullableOfArray = ( - objects: (SchemaObject | ReferenceObject)[] -) => (SchemaObject | ReferenceObject)[]; - export type MapNullableOfArrayWithNullable = ( objects: (SchemaObject | ReferenceObject)[], isNullable: boolean diff --git a/src/zod-extensions.ts b/src/zod-extensions.ts index b9e183d..f78d179 100644 --- a/src/zod-extensions.ts +++ b/src/zod-extensions.ts @@ -121,6 +121,7 @@ export function extendZodWithOpenApi(zod: typeof z) { // This zod instance is already extended with the required methods, // doing it again will just result in multiple wrapper methods for // `optional` and `nullable` + return; }