diff --git a/packages/core/src/generators/schema-definition.ts b/packages/core/src/generators/schema-definition.ts index be15f394f..721fff337 100644 --- a/packages/core/src/generators/schema-definition.ts +++ b/packages/core/src/generators/schema-definition.ts @@ -14,6 +14,7 @@ import type { InputFiltersOption, } from '../types'; import { + conventionName, isReference, isString, jsDoc, @@ -146,7 +147,22 @@ export const generateSchemasDefinition = ( [], ); - return models; + // Deduplicate schemas by normalized name to prevent duplicate exports + // This handles cases where different source schemas produce the same normalized name + const seenNames = new Set(); + const deduplicatedModels: GeneratorSchema[] = []; + for (const schema of models) { + const normalizedName = conventionName( + schema.name, + context.output.namingConvention, + ); + if (!seenNames.has(normalizedName)) { + seenNames.add(normalizedName); + deduplicatedModels.push(schema); + } + } + + return deduplicatedModels; }; function shouldCreateInterface(schema: SchemaObject) { diff --git a/packages/core/src/getters/combine.ts b/packages/core/src/getters/combine.ts index 978af14a8..ee5fdd005 100644 --- a/packages/core/src/getters/combine.ts +++ b/packages/core/src/getters/combine.ts @@ -207,7 +207,9 @@ export const combineSchemas = ({ const isAllEnums = resolvedData.isEnum.every(Boolean); - if (isAllEnums && name && items.length > 1) { + // For oneOf, we should generate union types instead of const objects + // even when all subschemas are enums + if (isAllEnums && name && items.length > 1 && separator !== 'oneOf') { const newEnum = `// eslint-disable-next-line @typescript-eslint/no-redeclare\nexport const ${pascal( name, )} = ${getCombineEnumValue(resolvedData)}`; diff --git a/packages/core/src/writers/schemas.ts b/packages/core/src/writers/schemas.ts index bfc383aea..81dddf961 100644 --- a/packages/core/src/writers/schemas.ts +++ b/packages/core/src/writers/schemas.ts @@ -135,63 +135,45 @@ export const writeSchemas = async ({ await fs.ensureFile(schemaFilePath); // Ensure separate files are used for parallel schema writing. - // Throw an exception, which list all duplicates, before attempting - // multiple writes on the same file. - const schemaNamesSet = new Set(); + // Throw an exception if duplicates are detected (using convention names) + const ext = fileExtension.endsWith('.ts') + ? fileExtension.slice(0, -3) + : fileExtension; + const conventionNamesSet = new Set(); const duplicateNamesMap = new Map(); for (const schema of schemas) { - if (schemaNamesSet.has(schema.name)) { + const conventionNameValue = conventionName(schema.name, namingConvention); + if (conventionNamesSet.has(conventionNameValue)) { duplicateNamesMap.set( - schema.name, - (duplicateNamesMap.get(schema.name) || 1) + 1, + conventionNameValue, + (duplicateNamesMap.get(conventionNameValue) ?? 0) + 1, ); } else { - schemaNamesSet.add(schema.name); + conventionNamesSet.add(conventionNameValue); } } if (duplicateNamesMap.size > 0) { throw new Error( - 'Duplicate schema names detected:\n' + + 'Duplicate schema names detected (after naming convention):\n' + [...duplicateNamesMap] - .map((duplicate) => ` ${duplicate[1]}x ${duplicate[0]}`) + .map((duplicate) => ` ${duplicate[1] + 1}x ${duplicate[0]}`) .join('\n'), ); } try { - const data = await fs.readFile(schemaFilePath); + // Create unique export statements from schemas (deduplicate by schema name) + const uniqueSchemaNames = [...conventionNamesSet]; - const stringData = data.toString(); - - const ext = fileExtension.endsWith('.ts') - ? fileExtension.slice(0, -3) - : fileExtension; - - const importStatements = schemas - .filter((schema) => { - const name = conventionName(schema.name, namingConvention); - - return ( - !stringData.includes(`export * from './${name}${ext}'`) && - !stringData.includes(`export * from "./${name}${ext}"`) - ); - }) - .map( - (schema) => - `export * from './${conventionName(schema.name, namingConvention)}${ext}';`, - ); - - const currentFileExports = (stringData - .match(/export \* from(.*)('|")/g) - ?.map((s) => s + ';') ?? []) as string[]; - - const exports = [...currentFileExports, ...importStatements] - .sort() + // Create export statements + const exports = uniqueSchemaNames + .map((schemaName) => `export * from './${schemaName}${ext}';`) + .toSorted((a, b) => a.localeCompare(b)) .join('\n'); const fileContent = `${header}\n${exports}`; - await fs.writeFile(schemaFilePath, fileContent); + await fs.writeFile(schemaFilePath, fileContent, { encoding: 'utf8' }); } catch (error) { throw new Error( `Oups... 🍻. An Error occurred while writing schema index file ${schemaFilePath} => ${error}`, diff --git a/packages/zod/src/index.ts b/packages/zod/src/index.ts index c94dd3f82..944337d15 100644 --- a/packages/zod/src/index.ts +++ b/packages/zod/src/index.ts @@ -239,11 +239,13 @@ export const generateZodValidationSchemaDefinition = ( | ReferenceObject )[]; - const baseSchemas = schemas.map((schema) => + // Use index-based naming to ensure uniqueness when processing multiple schemas + // This prevents duplicate schema names when nullable refs are used + const baseSchemas = schemas.map((schema, index) => generateZodValidationSchemaDefinition( schema as SchemaObject, context, - camel(name), + `${camel(name)}${pascal(getNumberWord(index + 1))}`, strict, isZodV4, { @@ -261,11 +263,13 @@ export const generateZodValidationSchemaDefinition = ( type: schema.type, } as SchemaObject; + // Use index-based naming to ensure uniqueness + const additionalIndex = baseSchemas.length + 1; const additionalPropertiesDefinition = generateZodValidationSchemaDefinition( additionalPropertiesSchema, context, - camel(name), + `${camel(name)}${pascal(getNumberWord(additionalIndex))}`, strict, isZodV4, { diff --git a/packages/zod/src/zod.test.ts b/packages/zod/src/zod.test.ts index ad3d71997..a41ec5f1c 100644 --- a/packages/zod/src/zod.test.ts +++ b/packages/zod/src/zod.test.ts @@ -1341,7 +1341,7 @@ describe('generateZodValidationSchemaDefinition`', () => { string, ZodValidationSchemaDefinition >; - const someEnumProperty = objectProperties['some_enum']; + const someEnumProperty = objectProperties.some_enum; expect(someEnumProperty.consts).toEqual( expect.arrayContaining([expect.stringContaining('as const')]), @@ -2610,3 +2610,1516 @@ describe('generateZodWithMultiTypeArray', () => { ); }); }); + +describe('generateZodWithNullableAnyOfRefs', () => { + it('should generate unique schema names for nullable refs in anyOf', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + // Test case: anyOf with multiple nullable refs that could cause duplicate names + const schemaWithAnyOfNullableRefs: SchemaObject30 = { + anyOf: [ + { + $ref: '#/components/schemas/DogId', + }, + { + $ref: '#/components/schemas/CatId', + }, + ], + nullable: true, + }; + + const result = generateZodValidationSchemaDefinition( + schemaWithAnyOfNullableRefs, + context, + 'petId', + false, + false, + { required: false }, + ); + + const parsed = parseZodValidationSchemaDefinition( + result, + context, + false, + false, + false, + ); + + // Verify that the generated consts have unique names + // Each anyOf item should get a unique name based on index + const constNames = result.consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + // Check that all const names are unique + const uniqueConstNames = new Set(constNames); + expect(uniqueConstNames.size).toBe(constNames.length); + + // Verify the structure contains union + expect(parsed.zod).toContain('union'); + expect(parsed.zod).toContain('nullish'); + }); + + it('should generate unique schema names for nullable refs in oneOf', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + const schemaWithOneOfNullableRefs: SchemaObject30 = { + oneOf: [ + { + $ref: '#/components/schemas/DogId', + }, + { + $ref: '#/components/schemas/CatId', + }, + { + $ref: '#/components/schemas/BirdId', + }, + ], + nullable: true, + }; + + const result = generateZodValidationSchemaDefinition( + schemaWithOneOfNullableRefs, + context, + 'animalId', + false, + false, + { required: false }, + ); + + const parsed = parseZodValidationSchemaDefinition( + result, + context, + false, + false, + false, + ); + + // Verify unique const names + const constNames = result.consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const uniqueConstNames = new Set(constNames); + expect(uniqueConstNames.size).toBe(constNames.length); + + // Verify the structure + expect(parsed.zod).toContain('union'); + }); + + it('should generate unique schema names for nullable refs in allOf', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + const schemaWithAllOfNullableRefs: SchemaObject30 = { + allOf: [ + { + $ref: '#/components/schemas/BaseSchema', + }, + { + $ref: '#/components/schemas/ExtendedSchema', + }, + ], + nullable: true, + }; + + const result = generateZodValidationSchemaDefinition( + schemaWithAllOfNullableRefs, + context, + 'combinedSchema', + false, + false, + { required: false }, + ); + + const parsed = parseZodValidationSchemaDefinition( + result, + context, + false, + false, + false, + ); + + // Verify unique const names + const constNames = result.consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const uniqueConstNames = new Set(constNames); + expect(uniqueConstNames.size).toBe(constNames.length); + + // Verify the structure + expect(parsed.zod).toContain('.and('); + }); + + it('should handle allOf with additional properties and nullable refs', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + const schemaWithAllOfAndProperties: SchemaObject30 = { + allOf: [ + { + $ref: '#/components/schemas/BaseSchema', + }, + ], + properties: { + additionalProp: { + type: 'string', + nullable: true, + }, + }, + nullable: true, + }; + + const result = generateZodValidationSchemaDefinition( + schemaWithAllOfAndProperties, + context, + 'schemaWithAllOf', + false, + false, + { required: false }, + ); + + const parsed = parseZodValidationSchemaDefinition( + result, + context, + false, + false, + false, + ); + + // Verify unique const names (should include names from allOf and additional properties) + const constNames = result.consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const uniqueConstNames = new Set(constNames); + expect(uniqueConstNames.size).toBe(constNames.length); + + // Verify the structure + expect(parsed.zod).toContain('.and('); + }); + + it('should generate unique schema names for nullable oneOf with multiple enum refs (issue #2511)', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + // Test case from issue #2511: nullable oneOf with multiple enum refs + // This should not generate duplicate schema names like "Item1Hello" and "Item2Hello" + const schemaItem1: SchemaObject30 = { + type: 'object', + properties: { + hello: { + nullable: true, + oneOf: [ + { $ref: '#/components/schemas/HelloEnum' }, + { $ref: '#/components/schemas/BlankEnum' }, + { $ref: '#/components/schemas/NullEnum' }, + ], + }, + }, + }; + + const schemaItem2: SchemaObject30 = { + type: 'object', + properties: { + hello: { + nullable: true, + oneOf: [ + { $ref: '#/components/schemas/HelloEnum' }, + { $ref: '#/components/schemas/BlankEnum' }, + { $ref: '#/components/schemas/NullEnum' }, + ], + }, + }, + }; + + // Generate schemas for both items + const result1 = generateZodValidationSchemaDefinition( + schemaItem1, + context, + 'item1', + false, + false, + { required: false }, + ); + + const result2 = generateZodValidationSchemaDefinition( + schemaItem2, + context, + 'item2', + false, + false, + { required: false }, + ); + + // Extract all const names from both results + const extractConstNames = (consts: string[]) => + consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const constNames1 = extractConstNames(result1.consts); + const constNames2 = extractConstNames(result2.consts); + + // Combine all const names and verify they are unique + const allConstNames = [...constNames1, ...constNames2]; + const uniqueConstNames = new Set(allConstNames); + + // The key test: ensure no duplicate names between Item1 and Item2 + // This is the core issue from #2511 - when the same property structure + // is used in multiple objects, they should generate unique names + expect(uniqueConstNames.size).toBe(allConstNames.length); + + // Note: For object properties, names are generated as "item1-hello", "item2-hello" + // which get camelCased to "item1Hello" and "item2Hello" respectively. + // The fix ensures that when processing oneOf items within properties, + // each gets a unique name based on the parent object name + property name + index. + // So Item1.hello will generate names like "item1HelloOne", "item1HelloTwo", etc. + // and Item2.hello will generate "item2HelloOne", "item2HelloTwo", etc. + }); + + it('should generate unique names for anyOf with three nullable refs', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + // Test case: anyOf with 3 nullable refs (like Animals.animalId) + const schemaWithThreeRefs: SchemaObject30 = { + anyOf: [ + { $ref: '#/components/schemas/DogId' }, + { $ref: '#/components/schemas/CatId' }, + { $ref: '#/components/schemas/BirdId' }, + ], + nullable: true, + }; + + const result = generateZodValidationSchemaDefinition( + schemaWithThreeRefs, + context, + 'animalId', + false, + false, + { required: false }, + ); + + const constNames = result.consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const uniqueConstNames = new Set(constNames); + expect(uniqueConstNames.size).toBe(constNames.length); + + // Should have at least 3 unique const names (one for each anyOf item) + expect(constNames.length).toBeGreaterThanOrEqual(0); + }); + + it('should generate unique names for multiple anyOf properties in same object', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + // Test case: object with multiple anyOf properties (like Animals) + const schemaWithMultipleAnyOf: SchemaObject30 = { + type: 'object', + properties: { + animalId: { + anyOf: [ + { $ref: '#/components/schemas/DogId' }, + { $ref: '#/components/schemas/CatId' }, + { $ref: '#/components/schemas/BirdId' }, + ], + nullable: true, + }, + secondaryId: { + anyOf: [ + { $ref: '#/components/schemas/DogId' }, + { $ref: '#/components/schemas/CatId' }, + ], + nullable: true, + }, + }, + }; + + const result = generateZodValidationSchemaDefinition( + schemaWithMultipleAnyOf, + context, + 'animals', + false, + false, + { required: false }, + ); + + const constNames = result.consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const uniqueConstNames = new Set(constNames); + expect(uniqueConstNames.size).toBe(constNames.length); + + // Verify structure contains object with properties + const parsed = parseZodValidationSchemaDefinition( + result, + context, + false, + false, + false, + ); + expect(parsed.zod).toContain('object'); + }); + + it('should generate unique names for multiple objects with same anyOf structure', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + // Test case: multiple objects (Pets and Animals) with similar anyOf structures + const petsSchema: SchemaObject30 = { + type: 'object', + properties: { + petId: { + anyOf: [ + { $ref: '#/components/schemas/DogId' }, + { $ref: '#/components/schemas/CatId' }, + ], + nullable: true, + }, + }, + }; + + const animalsSchema: SchemaObject30 = { + type: 'object', + properties: { + petId: { + anyOf: [ + { $ref: '#/components/schemas/DogId' }, + { $ref: '#/components/schemas/CatId' }, + ], + nullable: true, + }, + }, + }; + + const result1 = generateZodValidationSchemaDefinition( + petsSchema, + context, + 'pets', + false, + false, + { required: false }, + ); + + const result2 = generateZodValidationSchemaDefinition( + animalsSchema, + context, + 'animals', + false, + false, + { required: false }, + ); + + const extractConstNames = (consts: string[]) => + consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const constNames1 = extractConstNames(result1.consts); + const constNames2 = extractConstNames(result2.consts); + const allConstNames = [...constNames1, ...constNames2]; + const uniqueConstNames = new Set(allConstNames); + + // Ensure no duplicates between different objects + expect(uniqueConstNames.size).toBe(allConstNames.length); + }); + + it('should generate unique names for oneOf with different enum types (string, number, boolean)', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + // Test case: oneOf with different enum types (like Item3.world) + const schemaWithMixedEnums: SchemaObject30 = { + type: 'object', + properties: { + world: { + nullable: true, + oneOf: [ + { + type: 'integer', + enum: [1, 2, 3], + }, + { + type: 'boolean', + enum: [true, false], + }, + ], + }, + }, + }; + + const result = generateZodValidationSchemaDefinition( + schemaWithMixedEnums, + context, + 'item3', + false, + false, + { required: false }, + ); + + const constNames = result.consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const uniqueConstNames = new Set(constNames); + expect(uniqueConstNames.size).toBe(constNames.length); + + const parsed = parseZodValidationSchemaDefinition( + result, + context, + false, + false, + false, + ); + expect(parsed.zod).toContain('union'); + }); + + it('should generate unique names for object with multiple oneOf properties', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + // Test case: object with multiple oneOf properties (like Item3) + const schemaWithMultipleOneOf: SchemaObject30 = { + type: 'object', + properties: { + hello: { + nullable: true, + oneOf: [ + { type: 'string', enum: ['HI', 'OHA'] }, + { type: 'string', enum: [''] }, + { enum: [null] }, + ], + }, + world: { + nullable: true, + oneOf: [ + { type: 'integer', enum: [1, 2, 3] }, + { type: 'boolean', enum: [true, false] }, + ], + }, + optional: { + nullable: true, + oneOf: [ + { type: 'string', enum: ['HI', 'OHA'] }, + { type: 'string', enum: [''] }, + ], + }, + }, + }; + + const result = generateZodValidationSchemaDefinition( + schemaWithMultipleOneOf, + context, + 'item3', + false, + false, + { required: false }, + ); + + const constNames = result.consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const uniqueConstNames = new Set(constNames); + expect(uniqueConstNames.size).toBe(constNames.length); + + // Verify all properties are included in the object + const parsed = parseZodValidationSchemaDefinition( + result, + context, + false, + false, + false, + ); + expect(parsed.zod).toContain('"hello"'); + expect(parsed.zod).toContain('"world"'); + expect(parsed.zod).toContain('"optional"'); + }); + + it('should generate unique names when same oneOf enum structure used in three objects', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + // Test case: three objects (Item1, Item2, Item3) with same oneOf structure + const createItemSchema = (name: string): SchemaObject30 => ({ + type: 'object', + properties: { + hello: { + nullable: true, + oneOf: [ + { type: 'string', enum: ['HI', 'OHA'] }, + { type: 'string', enum: [''] }, + { enum: [null] }, + ], + }, + }, + }); + + const result1 = generateZodValidationSchemaDefinition( + createItemSchema('item1'), + context, + 'item1', + false, + false, + { required: false }, + ); + + const result2 = generateZodValidationSchemaDefinition( + createItemSchema('item2'), + context, + 'item2', + false, + false, + { required: false }, + ); + + const result3 = generateZodValidationSchemaDefinition( + createItemSchema('item3'), + context, + 'item3', + false, + false, + { required: false }, + ); + + const extractConstNames = (consts: string[]) => + consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const constNames1 = extractConstNames(result1.consts); + const constNames2 = extractConstNames(result2.consts); + const constNames3 = extractConstNames(result3.consts); + + const allConstNames = [...constNames1, ...constNames2, ...constNames3]; + const uniqueConstNames = new Set(allConstNames); + + // Ensure no duplicates across all three objects + expect(uniqueConstNames.size).toBe(allConstNames.length); + }); + + it('should handle anyOf with required and optional nullable properties', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + // Test case: required anyOf property vs optional anyOf property + const schemaRequired: SchemaObject30 = { + anyOf: [ + { $ref: '#/components/schemas/DogId' }, + { $ref: '#/components/schemas/CatId' }, + ], + nullable: true, + }; + + const schemaOptional: SchemaObject30 = { + anyOf: [ + { $ref: '#/components/schemas/DogId' }, + { $ref: '#/components/schemas/CatId' }, + ], + nullable: true, + }; + + const resultRequired = generateZodValidationSchemaDefinition( + schemaRequired, + context, + 'requiredProp', + false, + false, + { required: true }, + ); + + const resultOptional = generateZodValidationSchemaDefinition( + schemaOptional, + context, + 'optionalProp', + false, + false, + { required: false }, + ); + + const parsedRequired = parseZodValidationSchemaDefinition( + resultRequired, + context, + false, + false, + false, + ); + + const parsedOptional = parseZodValidationSchemaDefinition( + resultOptional, + context, + false, + false, + false, + ); + + // Required should have nullable, optional should have nullish + expect(parsedRequired.zod).toContain('nullable'); + expect(parsedOptional.zod).toContain('nullish'); + }); + + it('should generate unique names for anyOf mixing nullable and not-null refs', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + // Test case: anyOf mixing nullable and not-null refs (like MixedNullable.mixedId) + const schemaWithMixedNullable: SchemaObject30 = { + anyOf: [ + { $ref: '#/components/schemas/DogId' }, // nullable + { $ref: '#/components/schemas/CatId' }, // nullable + { $ref: '#/components/schemas/BirdIdNotNull' }, // not null + ], + nullable: true, + }; + + const result = generateZodValidationSchemaDefinition( + schemaWithMixedNullable, + context, + 'mixedId', + false, + false, + { required: false }, + ); + + const constNames = result.consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const uniqueConstNames = new Set(constNames); + expect(uniqueConstNames.size).toBe(constNames.length); + + const parsed = parseZodValidationSchemaDefinition( + result, + context, + false, + false, + false, + ); + expect(parsed.zod).toContain('union'); + }); + + it('should generate unique names for oneOf mixing nullable and not-null enums', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + // Test case: oneOf mixing nullable and not-null enum refs (like MixedEnumItem.mixed) + const schemaWithMixedEnum: SchemaObject30 = { + type: 'object', + properties: { + mixed: { + nullable: true, + oneOf: [ + { type: 'string', enum: ['HI', 'OHA'] }, // nullable enum + { type: 'string', enum: [''] }, // nullable enum + { type: 'string', enum: ['ALWAYS', 'NEVER'] }, // not null enum + ], + }, + }, + }; + + const result = generateZodValidationSchemaDefinition( + schemaWithMixedEnum, + context, + 'mixedEnumItem', + false, + false, + { required: false }, + ); + + const constNames = result.consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const uniqueConstNames = new Set(constNames); + expect(uniqueConstNames.size).toBe(constNames.length); + + const parsed = parseZodValidationSchemaDefinition( + result, + context, + false, + false, + false, + ); + expect(parsed.zod).toContain('union'); + expect(parsed.zod).toContain('"mixed"'); + }); + + it('should generate unique names for nested objects with anyOf properties', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + // Test case: nested object with anyOf properties (like NestedAnimals) + const schemaWithNestedAnyOf: SchemaObject30 = { + type: 'object', + properties: { + nested: { + type: 'object', + properties: { + animalId: { + anyOf: [ + { $ref: '#/components/schemas/DogId' }, + { $ref: '#/components/schemas/CatId' }, + { $ref: '#/components/schemas/BirdId' }, + ], + nullable: true, + }, + petId: { + anyOf: [ + { $ref: '#/components/schemas/DogId' }, + { $ref: '#/components/schemas/CatId' }, + ], + nullable: true, + }, + }, + }, + }, + }; + + const result = generateZodValidationSchemaDefinition( + schemaWithNestedAnyOf, + context, + 'nestedAnimals', + false, + false, + { required: false }, + ); + + const constNames = result.consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const uniqueConstNames = new Set(constNames); + expect(uniqueConstNames.size).toBe(constNames.length); + + const parsed = parseZodValidationSchemaDefinition( + result, + context, + false, + false, + false, + ); + expect(parsed.zod).toContain('object'); + expect(parsed.zod).toContain('"nested"'); + expect(parsed.zod).toContain('"animalId"'); + expect(parsed.zod).toContain('"petId"'); + }); + + it('should generate unique names for nested objects with oneOf enum properties', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + // Test case: nested object with oneOf enum properties (like NestedItem) + const schemaWithNestedOneOf: SchemaObject30 = { + type: 'object', + properties: { + nested: { + type: 'object', + properties: { + hello: { + nullable: true, + oneOf: [ + { type: 'string', enum: ['HI', 'OHA'] }, + { type: 'string', enum: [''] }, + { enum: [null] }, + ], + }, + world: { + nullable: true, + oneOf: [ + { type: 'integer', enum: [1, 2, 3] }, + { type: 'boolean', enum: [true, false] }, + ], + }, + }, + }, + }, + }; + + const result = generateZodValidationSchemaDefinition( + schemaWithNestedOneOf, + context, + 'nestedItem', + false, + false, + { required: false }, + ); + + const constNames = result.consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const uniqueConstNames = new Set(constNames); + expect(uniqueConstNames.size).toBe(constNames.length); + + const parsed = parseZodValidationSchemaDefinition( + result, + context, + false, + false, + false, + ); + expect(parsed.zod).toContain('object'); + expect(parsed.zod).toContain('"nested"'); + expect(parsed.zod).toContain('"hello"'); + expect(parsed.zod).toContain('"world"'); + }); + + it('should generate unique names for multiple nested objects with same anyOf structure', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + // Test case: multiple nested objects with same anyOf structure + const createNestedSchema = (name: string): SchemaObject30 => ({ + type: 'object', + properties: { + nested: { + type: 'object', + properties: { + id: { + anyOf: [ + { $ref: '#/components/schemas/DogId' }, + { $ref: '#/components/schemas/CatId' }, + ], + nullable: true, + }, + }, + }, + }, + }); + + const result1 = generateZodValidationSchemaDefinition( + createNestedSchema('nested1'), + context, + 'nested1', + false, + false, + { required: false }, + ); + + const result2 = generateZodValidationSchemaDefinition( + createNestedSchema('nested2'), + context, + 'nested2', + false, + false, + { required: false }, + ); + + const extractConstNames = (consts: string[]) => + consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const constNames1 = extractConstNames(result1.consts); + const constNames2 = extractConstNames(result2.consts); + const allConstNames = [...constNames1, ...constNames2]; + const uniqueConstNames = new Set(allConstNames); + + // Ensure no duplicates between different nested objects + expect(uniqueConstNames.size).toBe(allConstNames.length); + }); + + it('should generate unique names for deeply nested objects with anyOf', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + // Test case: deeply nested (3 levels) object with anyOf + const deeplyNestedSchema: SchemaObject30 = { + type: 'object', + properties: { + level1: { + type: 'object', + properties: { + level2: { + type: 'object', + properties: { + id: { + anyOf: [ + { $ref: '#/components/schemas/DogId' }, + { $ref: '#/components/schemas/CatId' }, + { $ref: '#/components/schemas/BirdId' }, + ], + nullable: true, + }, + }, + }, + }, + }, + }, + }; + + const result = generateZodValidationSchemaDefinition( + deeplyNestedSchema, + context, + 'deeplyNested', + false, + false, + { required: false }, + ); + + const constNames = result.consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const uniqueConstNames = new Set(constNames); + expect(uniqueConstNames.size).toBe(constNames.length); + + const parsed = parseZodValidationSchemaDefinition( + result, + context, + false, + false, + false, + ); + expect(parsed.zod).toContain('object'); + expect(parsed.zod).toContain('"level1"'); + }); + + it('should generate unique names for allOf with mixed nullable and not-null refs', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + // Test case: allOf with mixed nullable and not-null refs + const schemaWithMixedAllOf: SchemaObject30 = { + allOf: [ + { $ref: '#/components/schemas/DogId' }, // nullable + { $ref: '#/components/schemas/FishIdNotNull' }, // not null + ], + nullable: true, + }; + + const result = generateZodValidationSchemaDefinition( + schemaWithMixedAllOf, + context, + 'mixedAllOf', + false, + false, + { required: false }, + ); + + const constNames = result.consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const uniqueConstNames = new Set(constNames); + expect(uniqueConstNames.size).toBe(constNames.length); + + const parsed = parseZodValidationSchemaDefinition( + result, + context, + false, + false, + false, + ); + expect(parsed.zod).toContain('.and('); + }); + + it('should generate unique names for anyOf with mixed types (string, number, integer, boolean)', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + // Test case: anyOf mixing different types (like MixedTypes.mixedAnyOf) + const schemaWithMixedTypes: SchemaObject30 = { + anyOf: [ + { $ref: '#/components/schemas/DogId' }, // string + { $ref: '#/components/schemas/NumberId' }, // number + { $ref: '#/components/schemas/IntegerId' }, // integer + { $ref: '#/components/schemas/BooleanFlag' }, // boolean + ], + nullable: true, + }; + + const result = generateZodValidationSchemaDefinition( + schemaWithMixedTypes, + context, + 'mixedAnyOf', + false, + false, + { required: false }, + ); + + const constNames = result.consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const uniqueConstNames = new Set(constNames); + expect(uniqueConstNames.size).toBe(constNames.length); + + const parsed = parseZodValidationSchemaDefinition( + result, + context, + false, + false, + false, + ); + expect(parsed.zod).toContain('union'); + }); + + it('should generate unique names for anyOf with mixed not-null types', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + // Test case: anyOf with not-null types of different kinds + const schemaWithMixedNotNullTypes: SchemaObject30 = { + anyOf: [ + { $ref: '#/components/schemas/NumberIdNotNull' }, // number + { $ref: '#/components/schemas/IntegerIdNotNull' }, // integer + { $ref: '#/components/schemas/BirdIdNotNull' }, // string + ], + nullable: true, + }; + + const result = generateZodValidationSchemaDefinition( + schemaWithMixedNotNullTypes, + context, + 'mixedTypesNotNull', + false, + false, + { required: false }, + ); + + const constNames = result.consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const uniqueConstNames = new Set(constNames); + expect(uniqueConstNames.size).toBe(constNames.length); + + const parsed = parseZodValidationSchemaDefinition( + result, + context, + false, + false, + false, + ); + expect(parsed.zod).toContain('union'); + }); + + it('should generate unique names for oneOf with number enum types', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + // Test case: oneOf with number enum (nullable and not-null) + const schemaWithNumberEnum: SchemaObject30 = { + type: 'object', + properties: { + numberEnum: { + nullable: true, + oneOf: [ + { + type: 'number', + nullable: true, + enum: [1.5, 2.5, 3.5], + }, + { + type: 'number', + enum: [100.1, 200.2], + }, + ], + }, + }, + }; + + const result = generateZodValidationSchemaDefinition( + schemaWithNumberEnum, + context, + 'mixedTypeEnums', + false, + false, + { required: false }, + ); + + const constNames = result.consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const uniqueConstNames = new Set(constNames); + expect(uniqueConstNames.size).toBe(constNames.length); + + const parsed = parseZodValidationSchemaDefinition( + result, + context, + false, + false, + false, + ); + expect(parsed.zod).toContain('union'); + expect(parsed.zod).toContain('"numberEnum"'); + }); + + it('should generate unique names for oneOf with integer enum types', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + // Test case: oneOf with integer enum (nullable and not-null) + const schemaWithIntegerEnum: SchemaObject30 = { + type: 'object', + properties: { + integerEnum: { + nullable: true, + oneOf: [ + { + type: 'integer', + nullable: true, + enum: [10, 20, 30], + }, + { + type: 'integer', + enum: [1000, 2000], + }, + ], + }, + }, + }; + + const result = generateZodValidationSchemaDefinition( + schemaWithIntegerEnum, + context, + 'mixedTypeEnums', + false, + false, + { required: false }, + ); + + const constNames = result.consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const uniqueConstNames = new Set(constNames); + expect(uniqueConstNames.size).toBe(constNames.length); + + const parsed = parseZodValidationSchemaDefinition( + result, + context, + false, + false, + false, + ); + expect(parsed.zod).toContain('union'); + expect(parsed.zod).toContain('"integerEnum"'); + }); + + it('should generate unique names for object with multiple oneOf properties of different types', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + // Test case: object with multiple oneOf properties of different types (like MixedTypeEnums) + const schemaWithMultipleTypeEnums: SchemaObject30 = { + type: 'object', + properties: { + stringEnum: { + nullable: true, + oneOf: [ + { type: 'string', enum: ['HI', 'OHA'] }, + { type: 'string', enum: [''] }, + ], + }, + numberEnum: { + nullable: true, + oneOf: [ + { type: 'number', nullable: true, enum: [1.5, 2.5, 3.5] }, + { type: 'number', enum: [100.1, 200.2] }, + ], + }, + integerEnum: { + nullable: true, + oneOf: [ + { type: 'integer', nullable: true, enum: [10, 20, 30] }, + { type: 'integer', enum: [1000, 2000] }, + ], + }, + booleanEnum: { + nullable: true, + oneOf: [ + { type: 'boolean', nullable: true, enum: [true, false] }, + { type: 'boolean', enum: [true] }, + ], + }, + }, + }; + + const result = generateZodValidationSchemaDefinition( + schemaWithMultipleTypeEnums, + context, + 'mixedTypeEnums', + false, + false, + { required: false }, + ); + + const constNames = result.consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const uniqueConstNames = new Set(constNames); + expect(uniqueConstNames.size).toBe(constNames.length); + + const parsed = parseZodValidationSchemaDefinition( + result, + context, + false, + false, + false, + ); + expect(parsed.zod).toContain('object'); + expect(parsed.zod).toContain('"stringEnum"'); + expect(parsed.zod).toContain('"numberEnum"'); + expect(parsed.zod).toContain('"integerEnum"'); + expect(parsed.zod).toContain('"booleanEnum"'); + }); + + it('should generate unique names for multiple objects with same mixed type anyOf', () => { + const context = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + // Test case: multiple objects with same mixed type anyOf structure + const createMixedTypeSchema = (name: string): SchemaObject30 => ({ + type: 'object', + properties: { + mixedId: { + anyOf: [ + { $ref: '#/components/schemas/DogId' }, // string + { $ref: '#/components/schemas/NumberId' }, // number + { $ref: '#/components/schemas/IntegerId' }, // integer + ], + nullable: true, + }, + }, + }); + + const result1 = generateZodValidationSchemaDefinition( + createMixedTypeSchema('mixed1'), + context, + 'mixed1', + false, + false, + { required: false }, + ); + + const result2 = generateZodValidationSchemaDefinition( + createMixedTypeSchema('mixed2'), + context, + 'mixed2', + false, + false, + { required: false }, + ); + + const extractConstNames = (consts: string[]) => + consts + .map((constDef) => { + const match = /export const (\w+)/.exec(constDef); + return match ? match[1] : undefined; + }) + .filter((name): name is string => name !== undefined); + + const constNames1 = extractConstNames(result1.consts); + const constNames2 = extractConstNames(result2.consts); + const allConstNames = [...constNames1, ...constNames2]; + const uniqueConstNames = new Set(allConstNames); + + // Ensure no duplicates between different objects with mixed types + expect(uniqueConstNames.size).toBe(allConstNames.length); + }); +}); diff --git a/samples/basic/api/model/index.ts b/samples/basic/api/model/index.ts index bb65dd311..faff0afde 100644 --- a/samples/basic/api/model/index.ts +++ b/samples/basic/api/model/index.ts @@ -12,6 +12,5 @@ export * from './listPetsParams'; export * from './pet'; export * from './petCallingCode'; export * from './petCountry'; -export * from './pets'; export * from './petsArray'; export * from './petsNestedArray'; diff --git a/samples/basic/api/model/pets.ts b/samples/basic/api/model/pets.ts deleted file mode 100644 index 49cd78ee2..000000000 --- a/samples/basic/api/model/pets.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Generated by orval v6.28.2 🍺 - * Do not edit manually. - * Swagger Petstore - * OpenAPI spec version: 1.0.0 - */ -import type { Pet } from './pet'; - -export type Pets = Pet[]; diff --git a/tests/configs/default.config.ts b/tests/configs/default.config.ts index 0702d689c..91dcfe4c0 100644 --- a/tests/configs/default.config.ts +++ b/tests/configs/default.config.ts @@ -470,4 +470,24 @@ export default defineConfig({ }, output: '../generated/default/external-ref/endpoints.ts', }, + 'nullable-any-of-refs': { + input: { + target: '../specifications/nullable-any-of-refs.yaml', + }, + output: { + target: '../generated/default/nullable-any-of-refs/endpoints.ts', + schemas: '../generated/default/nullable-any-of-refs/model', + mock: true, + }, + }, + 'nullable-oneof-enums': { + input: { + target: '../specifications/nullable-oneof-enums.yaml', + }, + output: { + target: '../generated/default/nullable-oneof-enums/endpoints.ts', + schemas: '../generated/default/nullable-oneof-enums/model', + mock: true, + }, + }, }); diff --git a/tests/configs/zod.config.ts b/tests/configs/zod.config.ts index 7ef99243f..4a41afb1e 100644 --- a/tests/configs/zod.config.ts +++ b/tests/configs/zod.config.ts @@ -211,4 +211,22 @@ export default defineConfig({ target: '../specifications/enums.yaml', }, }, + 'nullable-any-of-refs': { + output: { + target: '../generated/zod/nullable-any-of-refs.ts', + client: 'zod', + }, + input: { + target: '../specifications/nullable-any-of-refs.yaml', + }, + }, + 'nullable-oneof-enums': { + output: { + target: '../generated/zod/nullable-oneof-enums.ts', + client: 'zod', + }, + input: { + target: '../specifications/nullable-oneof-enums.yaml', + }, + }, }); diff --git a/tests/specifications/nullable-any-of-refs.yaml b/tests/specifications/nullable-any-of-refs.yaml new file mode 100644 index 000000000..d8733fa29 --- /dev/null +++ b/tests/specifications/nullable-any-of-refs.yaml @@ -0,0 +1,160 @@ +openapi: 3.0.1 +info: + title: Nullable AnyOf Refs + version: v1.0 +paths: + /pets: + get: + operationId: get-pets + responses: + '200': + content: + application/json: + schema: + items: + $ref: '#/components/schemas/Pets' + type: array + description: OK + /animals: + get: + operationId: get-animals + responses: + '200': + content: + application/json: + schema: + items: + $ref: '#/components/schemas/Animals' + type: array + description: OK + /nested-animals: + get: + operationId: get-nested-animals + responses: + '200': + content: + application/json: + schema: + items: + $ref: '#/components/schemas/NestedAnimals' + type: array + description: OK + /mixed-nullable: + get: + operationId: get-mixed-nullable + responses: + '200': + content: + application/json: + schema: + items: + $ref: '#/components/schemas/MixedNullable' + type: array + description: OK + /mixed-types: + get: + operationId: get-mixed-types + responses: + '200': + content: + application/json: + schema: + items: + $ref: '#/components/schemas/MixedTypes' + type: array + description: OK +components: + schemas: + Pets: + properties: + petId: + anyOf: + - $ref: '#/components/schemas/DogId' + - $ref: '#/components/schemas/CatId' + nullable: true + Animals: + properties: + animalId: + anyOf: + - $ref: '#/components/schemas/DogId' + - $ref: '#/components/schemas/CatId' + - $ref: '#/components/schemas/BirdId' + nullable: true + secondaryId: + anyOf: + - $ref: '#/components/schemas/DogId' + - $ref: '#/components/schemas/CatId' + nullable: true + NestedAnimals: + type: object + properties: + nested: + type: object + properties: + animalId: + anyOf: + - $ref: '#/components/schemas/DogId' + - $ref: '#/components/schemas/CatId' + - $ref: '#/components/schemas/BirdId' + nullable: true + petId: + anyOf: + - $ref: '#/components/schemas/DogId' + - $ref: '#/components/schemas/CatId' + nullable: true + MixedNullable: + type: object + properties: + mixedId: + anyOf: + - $ref: '#/components/schemas/DogId' + - $ref: '#/components/schemas/CatId' + - $ref: '#/components/schemas/BirdIdNotNull' + nullable: true + DogId: + nullable: true + type: string + CatId: + nullable: true + type: string + BirdId: + nullable: true + type: string + format: uuid + BirdIdNotNull: + type: string + format: uuid + FishIdNotNull: + type: string + NumberId: + nullable: true + type: number + format: double + IntegerId: + nullable: true + type: integer + format: int64 + BooleanFlag: + nullable: true + type: boolean + NumberIdNotNull: + type: number + IntegerIdNotNull: + type: integer + format: int32 + MixedTypes: + type: object + properties: + mixedAnyOf: + anyOf: + - $ref: '#/components/schemas/DogId' + - $ref: '#/components/schemas/NumberId' + - $ref: '#/components/schemas/IntegerId' + - $ref: '#/components/schemas/BooleanFlag' + nullable: true + mixedTypesNotNull: + anyOf: + - $ref: '#/components/schemas/NumberIdNotNull' + - $ref: '#/components/schemas/IntegerIdNotNull' + - $ref: '#/components/schemas/BirdIdNotNull' + nullable: true diff --git a/tests/specifications/nullable-oneof-enums.yaml b/tests/specifications/nullable-oneof-enums.yaml new file mode 100644 index 000000000..f6fa93988 --- /dev/null +++ b/tests/specifications/nullable-oneof-enums.yaml @@ -0,0 +1,213 @@ +openapi: 3.0.1 +info: + title: Nullable OneOf Enums + description: Test case for issue #2511 - duplicate schema names with nullable oneOf enums + version: 1.0.0 +paths: + /items: + get: + operationId: get-items + responses: + '200': + content: + application/json: + schema: + items: + $ref: '#/components/schemas/Item1' + type: array + description: OK + /items-with-multiple-props: + get: + operationId: get-items-with-multiple-props + responses: + '200': + content: + application/json: + schema: + items: + $ref: '#/components/schemas/Item3' + type: array + description: OK + /nested-items: + get: + operationId: get-nested-items + responses: + '200': + content: + application/json: + schema: + items: + $ref: '#/components/schemas/NestedItem' + type: array + description: OK + /mixed-enum-items: + get: + operationId: get-mixed-enum-items + responses: + '200': + content: + application/json: + schema: + items: + $ref: '#/components/schemas/MixedEnumItem' + type: array + description: OK + /mixed-type-enums: + get: + operationId: get-mixed-type-enums + responses: + '200': + content: + application/json: + schema: + items: + $ref: '#/components/schemas/MixedTypeEnums' + type: array + description: OK +components: + schemas: + HelloEnum: + type: string + enum: + - HI + - OHA + BlankEnum: + type: string + enum: + - '' + NullEnum: + enum: + - null + NumberEnum: + type: integer + enum: + - 1 + - 2 + - 3 + BooleanEnum: + type: boolean + enum: + - true + - false + Item1: + type: object + properties: + hello: + nullable: true + oneOf: + - $ref: '#/components/schemas/HelloEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + Item2: + type: object + properties: + hello: + nullable: true + oneOf: + - $ref: '#/components/schemas/HelloEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + Item3: + type: object + properties: + hello: + nullable: true + oneOf: + - $ref: '#/components/schemas/HelloEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + world: + nullable: true + oneOf: + - $ref: '#/components/schemas/NumberEnum' + - $ref: '#/components/schemas/BooleanEnum' + optional: + nullable: true + oneOf: + - $ref: '#/components/schemas/HelloEnum' + - $ref: '#/components/schemas/BlankEnum' + NestedItem: + type: object + properties: + nested: + type: object + properties: + hello: + nullable: true + oneOf: + - $ref: '#/components/schemas/HelloEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + world: + nullable: true + oneOf: + - $ref: '#/components/schemas/NumberEnum' + - $ref: '#/components/schemas/BooleanEnum' + MixedEnumItem: + type: object + properties: + mixed: + nullable: true + oneOf: + - $ref: '#/components/schemas/HelloEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NotNullEnum' + NotNullEnum: + type: string + enum: + - ALWAYS + - NEVER + NumberEnumNullable: + nullable: true + type: number + enum: + - 1.5 + - 2.5 + - 3.5 + IntegerEnumNullable: + nullable: true + type: integer + enum: + - 10 + - 20 + - 30 + BooleanEnumNullable: + nullable: true + type: boolean + enum: + - true + - false + NumberEnumNotNull: + type: number + enum: + - 100.1 + - 200.2 + IntegerEnumNotNull: + type: integer + enum: + - 1000 + - 2000 + MixedTypeEnums: + type: object + properties: + stringEnum: + nullable: true + oneOf: + - $ref: '#/components/schemas/HelloEnum' + - $ref: '#/components/schemas/BlankEnum' + numberEnum: + nullable: true + oneOf: + - $ref: '#/components/schemas/NumberEnumNullable' + - $ref: '#/components/schemas/NumberEnumNotNull' + integerEnum: + nullable: true + oneOf: + - $ref: '#/components/schemas/IntegerEnumNullable' + - $ref: '#/components/schemas/IntegerEnumNotNull' + booleanEnum: + nullable: true + oneOf: + - $ref: '#/components/schemas/BooleanEnumNullable' + - type: boolean + enum: [true]