diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index e8f7eb86e02a..7933cd2a92ff 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -3869,9 +3869,93 @@ protected CodegenDiscriminator createDiscriminator(String schemaName, Schema sch * @param discriminatorPropertyName The name of the discriminator property. */ protected String getDiscriminatorPropertyType(Schema schema, String discriminatorPropertyName) { - return DiscriminatorUtils.getDiscriminatorPropertyType(schema, discriminatorPropertyName) + String type = DiscriminatorUtils.getDiscriminatorPropertyType(schema, discriminatorPropertyName) .map(this::toModelName) - .orElseGet(() -> typeMapping.get("string")); + .orElse(null); + if (type != null) { + return type; + } + List schemas = DiscriminatorUtils.getDistinctTypes(openAPI, schema, discriminatorPropertyName); + return getCommonSchemaType(schemas); + } + + /** + * Get the most commons denominator schemaType for several schemas. + *

+ * @param schemas the list of schemas to compare. + * + * @Return the comman type + */ + protected String getCommonSchemaType(List schemas) { + switch (schemas.size()) { + case 0: + //. keep string for backward compatibility + return typeMapping.get("string"); + case 1: + + Schema first = schemas.get(0); + try { + if (StringUtils.isNotEmpty(first.get$ref())) { + return toModelName(ModelUtils.getSimpleRef(first.get$ref())); + } + if (ModelUtils.isEnumSchema(first)) { + // inline enum, use the least common denominator + String simpleType = typeMapping.get("enum"); + + return simpleType != null? simpleType: typeMapping.get("object"); + } + return typeMapping.get(getPrimitiveType(first)); + } catch (Exception e) { + // fallback for some unit test misconfigurations... + LOGGER.warn("Unable to model name for " + first, e); + return typeMapping.get("string"); + } + default: + break; + } + +// boolean allRef = schemas.stream().allMatch(s -> s.get$ref() != null); +// if (allRef) { +// Set modelNames = schemas.stream() +// .map(s -> toModelName(ModelUtils.getSimpleRef(s.get$ref()))) +// .filter(Objects::nonNull) +// .collect(Collectors.toSet()); +// if (modelNames.size() == 1) { +// return modelNames.iterator().next(); +// } +// } + schemas = schemas.stream().map(s -> ModelUtils.getReferencedSchema(openAPI, s)).collect(Collectors.toList()); + return getCommonTypeMapping(schemas); + } + + /** + * Find the common type between different schemas. + * + * @param schemas list of more than one schema. + * @return the common matching mapped type + */ + protected String getCommonTypeMapping(List schemas) { + String simpleType = "object"; + + boolean allEnums = schemas.stream().allMatch(ModelUtils::isEnumSchema); + if (allEnums) { + // non matching enums. Use enum if if the langage can map it. + if (typeMapping.containsKey("enum")) { + simpleType = "enum"; + } + return typeMapping.get(simpleType); + } + if (schemas.stream().noneMatch(ModelUtils::isEnumSchema)) { + Set types = schemas.stream().map(this::getPrimitiveType).collect(Collectors.toSet()); + if (types.size() == 1) { + String foundPrimitiveType = types.iterator().next(); + if (typeMapping.containsKey(foundPrimitiveType)) { + // matching simple type + simpleType = foundPrimitiveType; + } + } + } + return typeMapping.get(simpleType); } /** diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java index 554f98183fc7..12f6784baebe 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java @@ -304,6 +304,7 @@ public AbstractJavaCodegen() { typeMapping.put("date", "Date"); typeMapping.put("file", "File"); typeMapping.put("AnyType", "Object"); + typeMapping.put("enum", "Enum"); importMapping.put("BigDecimal", "java.math.BigDecimal"); importMapping.put("UUID", "java.util.UUID"); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractKotlinCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractKotlinCodegen.java index d62c812ad86e..52fafa631674 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractKotlinCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractKotlinCodegen.java @@ -490,6 +490,16 @@ public Map postProcessAllModels(Map objs) return objs; } + /** + * Kotlin has its own implementation in postProcessAllModels + * + * @return + */ + @Override + protected String getCommonSchemaType(List schemas) { + return typeMapping.get("string"); + } + @Override public ModelsMap postProcessModels(ModelsMap objs) { objs = super.postProcessModelsEnum(objs); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/DiscriminatorUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/DiscriminatorUtils.java index 204db9346329..6ef83fe59f33 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/DiscriminatorUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/DiscriminatorUtils.java @@ -10,6 +10,7 @@ import org.slf4j.helpers.MessageFormatter; import java.util.*; +import java.util.stream.Collectors; import static org.openapitools.codegen.CodegenConstants.X_DISCRIMINATOR_VALUE; import static org.openapitools.codegen.utils.OnceLogger.once; @@ -322,6 +323,132 @@ private static CodegenProperty getDiscriminatorCodegenProperty(OpenAPI openAPI, return null; } + /** + * Get the best matching simple types for all the mapped schemas + * @param openAPI The openAPI specification + * @param schema The Schema that may contain the discriminator + * @param discPropName The String that is the discriminator propertyName in the schema + * @return the distinct list of property Schema with the requested discPropName + */ + public static List getDistinctTypes(OpenAPI openAPI, Schema schema, String discPropName) { + List mappedSchemas = getMappedSchemas(openAPI, schema); + List properties = new ArrayList<>(); + for (Schema s: mappedSchemas) { + Schema prop = findProperty(openAPI, s, discPropName, new HashSet<>()); + if (prop != null && properties.stream().noneMatch(p -> + Objects.equals(p.getType(), prop.getType()) && + Objects.equals(p.get$ref(), prop.get$ref()) && + Objects.equals(p.getEnum(), prop.getEnum()) && + Objects.equals(p.getFormat(), prop.getFormat()))) { + properties.add(prop); + } + } + return properties; + } + + /** + * return the list of dereferenced mapping schemas. + * @return the schemas found or empty list if not found. + */ + public static List getMappedSchemas(OpenAPI openAPI, Schema schema) { + if (schema.getDiscriminator() != null) { + if (schema.getDiscriminator().getMapping() != null) { + return schema.getDiscriminator().getMapping().values().stream() + .map(ref -> getReferencedSchema(openAPI, ref)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + if (schema.getOneOf() != null && !schema.getOneOf().isEmpty()) { + // try also oneOf without discriminator mapping + List oneOfs = (List) schema.getOneOf(); + return oneOfs.stream() + .filter(Objects::nonNull) + .map(oneOf -> oneOf.get$ref()!=null + ? getReferencedSchema(openAPI, oneOf.get$ref()) + : oneOf) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + } + return Collections.emptyList(); + } + + private static Schema getReferencedSchema(OpenAPI openAPI, String ref) { + if (ref == null) { + return null; + } + if (ref.indexOf('/') >= 0) { + ref = ModelUtils.getSimpleRef(ref); + } + return ModelUtils.getSchema(openAPI, ref); + } + + /** + * Recursively try to find the schema matching the propertyName. + * + * @return the schema found or null if not found + */ + public static Schema findProperty(OpenAPI openAPI, Schema schema, String propertyName, Set visitedSchemas) { + schema = ModelUtils.getReferencedSchema(openAPI, schema); + if (propertyName == null || schema == null || visitedSchemas.contains(schema)) { + return null; + } + visitedSchemas.add(schema); + Map properties = schema.getProperties(); + if (properties != null) { + Schema property = properties.get(propertyName); + if (property != null) { + Schema found = simplifyProperty(property, visitedSchemas); + return found; + } + } + // loop into parent allOfs + List allOfs = schema.getAllOf(); + if (allOfs != null) { + for (Schema child : allOfs) { + Schema found = findProperty(openAPI, child, propertyName, visitedSchemas); + if (found != null) { + return simplifyProperty(found, visitedSchemas); + } + } + } + return null; + } + + /** + * simplify allOf property represented as [$ref, {description: ...}]. + */ + private static Schema simplifyProperty(Schema schema, Set visitedSchemas) { + if (!ModelUtils.isAllOf(schema)) { + return schema; + } + if (visitedSchemas.contains(schema)) { + return null; + } + visitedSchemas.add(schema); + + /* + * handle type+description. For Example: + * petType: + * $ref: '#/components/schemas/PetType' + * description: DOG + */ + Schema found = null; + int count = 0; + for (Schema sc: (List)schema.getAllOf()) { + if (ModelUtils.getType(sc) != null || ModelUtils.hasRef(sc)) { + found = sc; + count++; + } + } + if (count == 1) { + return found; + } + // multiple types, too complex. + return null; + + } + public static class DiscriminatorData { private final Discriminator discriminator; diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/JavaFileAssert.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/JavaFileAssert.java index bd9c5b17922d..97df1e81c4cf 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/JavaFileAssert.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/JavaFileAssert.java @@ -1,5 +1,6 @@ package org.openapitools.codegen.java.assertions; +import com.github.javaparser.ParseProblemException; import com.github.javaparser.StaticJavaParser; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.Node; @@ -36,7 +37,7 @@ public static JavaFileAssert assertThat(final String source) { public static JavaFileAssert assertThat(final Path path) { try { return new JavaFileAssert(StaticJavaParser.parse(path)); - } catch (IOException e) { + } catch (IOException | ParseProblemException e) { throw new RuntimeException("Exception while reading file: " + path, e); } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java index b7184f853396..0bd582e30076 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java @@ -9335,4 +9335,67 @@ public void issue_24232() throws IOException { JavaFileAssert.assertThat(files.get("Dummy.java")) .fileContains("import org.myorg.MyCustomId;", "import org.myorg.MyCustomKey;"); } + + @Test + public void issue_24769() throws IOException { + Map files = generateFromContract( + "src/test/resources/3_0/oneOf_issue_24769.yaml", SPRING_BOOT, + Map.of(USE_SPRING_BOOT4, true) + ); + + JavaFileAssert.assertThat(files.get("Dog.java")) + .fileContains("public enum TypeEnum {", + "DOG(\"DOG\");"); + JavaFileAssert.assertThat(files.get("Cat.java")) + .fileContains("public enum TypeEnum {", + "CAT(\"CAT\");"); + JavaFileAssert.assertThat(files.get("Pet.java")) + .fileDoesNotContain("public enum TypeEnum {") + .fileContains("public Enum getType();"); + + JavaFileAssert.assertThat(files.get("PetInteger.java")) + .fileContains("public Integer getIntType();"); + + JavaFileAssert.assertThat(files.get("PetEnumRef.java")) + .fileContains("public PetEnumType getEnumRefType();"); + + JavaFileAssert.assertThat(files.get("PetWithParent.java")) + .fileContains("public PetEnumType getPetType();"); + + JavaFileAssert.assertThat(files.get("PetWithAllOf.java")) + .fileContains("public PetEnumType getTypeAllOf()"); + + JavaFileAssert.assertThat(files.get("PetWithEnum.java")) + .fileContains("public Enum getEnumType()"); + JavaFileAssert.assertThat(files.get("CatWithEnum.java")) + .fileContains("public enum EnumTypeEnum {", + "CAT(\"CAT\");"); + + + JavaFileAssert.assertThat(files.get("PetNoMapping.java")) + .fileContains("public Enum getType();"); + + } + + @DataProvider(name = "oneOfDiscriminatorType") + public Object[][] oneOfDiscriminatorType() { + return new Object[][]{ + {"/3_0/oneOf_issue_19194.yaml", true, "CargoInterface.java", "public CargoGeneralParameterUnit getUnit();"}, + {"/3_0/oneOf_issue_19194.yaml", false, "CargoInterface.java", "public Enum getUnit();"}, + {"/3_0/oneOf_issue_19194_v2.yaml", false, "CargoParent.java", "public Object getUnit()"}, + {"/3_0/oneof_polymorphism_and_inheritance.yaml", false, "FooRefOrValue.java", "public String getAtType()"} + }; + } + + @Test(dataProvider = "oneOfDiscriminatorType") + public void oneOfDiscriminatorType(String filename, boolean resolveInlineEnum, String fileToCheck, String expectedContains) throws IOException { + Map files = generateFromContract( + "src/test/resources" + filename, SPRING_BOOT, + Map.of(USE_SPRING_BOOT4, true), configurator-> + configurator.addInlineSchemaOption("RESOLVE_INLINE_ENUMS", Boolean.toString(resolveInlineEnum)) + ); + JavaFileAssert.assertThat(files.get(fileToCheck)) + .fileContains(expectedContains); + } + } diff --git a/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_19194.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_19194.yaml new file mode 100644 index 000000000000..837312b10c40 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_19194.yaml @@ -0,0 +1,83 @@ +openapi: 3.0.3 +info: + title: test + contact: + email: support@company.de + license: + name: company Licence + url: https://company.de/ + version: 0.1.1 +servers: + - url: https://webservice.company.org +paths: + /doSomething: + post: + summary: test + description: description for test operation + operationId: doSomething + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/doSomethingRequest' + required: true + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/doSomethingResponse' +components: + schemas: + doSomethingRequest: + title: doSomethingRequest + type: object + properties: + cargo: + $ref: '#/components/schemas/cargoInterface' + required: + - cargo + doSomethingResponse: + title: doSomethingResponse + type: object + cargoInterface: + oneOf: + - $ref: '#/components/schemas/TONS' + - $ref: '#/components/schemas/KILOGRAMS' + discriminator: + propertyName: unit + TONS: + allOf: + - $ref: '#/components/schemas/cargoGeneralParameter' + - type: object + properties: + amount: + type: integer + required: + - amount + KILOGRAMS: + allOf: + - $ref: '#/components/schemas/cargoGeneralParameter' + - type: object + properties: + amount: + type: number + format: double + example: 1000 + minimum: 0 + required: + - amount + cargoGeneralParameter: + type: object + properties: + unit: + type: string + enum: + - KILOGRAMS + - TONS + cargoId: + type: integer + required: + - unit + - cargoId \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_19194_v2.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_19194_v2.yaml new file mode 100644 index 000000000000..5f1f69d91229 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_19194_v2.yaml @@ -0,0 +1,88 @@ +openapi: 3.0.3 +info: + title: test + contact: + email: support@company.de + license: + name: company Licence + url: https://company.de/ + version: 0.1.1 +servers: + - url: https://webservice.company.org +paths: + /doSomething: + post: + summary: test + description: description for test operation + operationId: doSomething + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/doSomethingRequest' + required: true + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/doSomethingResponse' +components: + schemas: + doSomethingRequest: + title: doSomethingRequest + type: object + properties: + cargo: + $ref: '#/components/schemas/cargoParent' + required: + - cargo + doSomethingResponse: + title: doSomethingResponse + type: object + TonsUnitEnum: + type: string + enum: + - Tons + TONS: + title: TONS + type: object + properties: + unit: + $ref: '#/components/schemas/TonsUnitEnum' + amount: + type: number + format: double + example: 40 + minimum: 0 + required: + - unit + - amount + KILOGRAMS: + title: KILOGRAMS + type: object + properties: + unit: + type: string + pattern: ^KILOGRAMS$ + example: KILOGRAMS + amount: + type: number + format: double + example: 1000 + minimum: 0 + required: + - unit + - amount + cargoParent: + title: cargoParent + type: object + oneOf: + - $ref: '#/components/schemas/TONS' + - $ref: '#/components/schemas/KILOGRAMS' + discriminator: + propertyName: unit + mapping: + TONS: '#/components/schemas/TONS' + KILOGRAMS: '#/components/schemas/KILOGRAMS' \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_24769.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_24769.yaml new file mode 100644 index 000000000000..131faa030a6b --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_24769.yaml @@ -0,0 +1,248 @@ +openapi: 3.1.0 +info: + title: discriminator_constants + version: '1.0.0' +components: + schemas: + Pet: + oneOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Dog' + discriminator: + propertyName: type + mapping: + CAT: '#/components/schemas/Cat' + DOG: '#/components/schemas/Dog' + Cat: + description: A representation of a cat + type: object + properties: + type: + type: string + const: CAT + huntingSkill: + type: string + required: + - type + - huntingSkill + Dog: + description: A representation of a dog + type: object + properties: + type: + type: string + const: DOG + packSize: + type: integer + format: int32 + required: + - type + - packSize + PetInteger: + oneOf: + - $ref: '#/components/schemas/CatInteger' + - $ref: '#/components/schemas/DogInteger' + discriminator: + propertyName: intType + mapping: + 1: '#/components/schemas/CatInteger' + 2: '#/components/schemas/DogInteger' + CatInteger: + description: A representation of a cat + type: object + properties: + intType: + type: integer + huntingSkill: + type: string + required: + - intType + - huntingSkill + DogInteger: + description: A representation of a dog + type: object + properties: + intType: + type: integer + packSize: + type: integer + format: int32 + required: + - intType + - packSize + PetEnumRef: + oneOf: + - $ref: '#/components/schemas/CatEnumRef' + - $ref: '#/components/schemas/DogEnumRef' + discriminator: + propertyName: enumRefType + mapping: + CAT: '#/components/schemas/CatEnumRef' + DOG: '#/components/schemas/DogEnumRef' + CatEnumRef: + description: A representation of a cat + type: object + properties: + enumRefType: + $ref: '#/components/schemas/PetEnumType' + huntingSkill: + type: string + required: + - enumRefType + - huntingSkill + DogEnumRef: + description: A representation of a dog + type: object + properties: + enumRefType: + $ref: '#/components/schemas/PetEnumType' + description: dog type + packSize: + type: integer + format: int32 + required: + - enumRefType + - packSize + PetEnumType: + type: string + enum: + - CAT + - DOG + ParentType: + type: object + properties: + petType: + $ref: '#/components/schemas/PetEnumType' + PetWithParent: + oneOf: + - $ref: '#/components/schemas/CatWithParent' + - $ref: '#/components/schemas/DogWithParent' + discriminator: + propertyName: petType + mapping: + CAT: '#/components/schemas/CatWithParent' + DOG: '#/components/schemas/DogWithParent' + CatWithParent: + description: A representation of a cat + allOf: + - $ref: '#/components/schemas/ParentType' + - type: object + properties: + huntingSkill: + type: string + required: + - petType + - huntingSkill + DogWithParent: + description: A representation of a dog + allOf: + - $ref: '#/components/schemas/ParentType' + - type: object + properties: + packSize: + type: integer + format: int32 + required: + - petType + - packSize + PetWithAllOf: + oneOf: + - $ref: '#/components/schemas/CatWithAllOf' + - $ref: '#/components/schemas/DogWithAllOf' + discriminator: + propertyName: typeAllOf + CatWithAllOf: + description: A representation of a cat + type: object + properties: + typeAllOf: + allOf: + - $ref: '#/components/schemas/PetEnumType' + - deprecated: true + description: CAT + huntingSkill: + type: string + required: + - typeAllOf + - huntingSkill + PetWithEnum: + oneOf: + - $ref: '#/components/schemas/CatWithEnum' + - $ref: '#/components/schemas/DogWithEnum' + discriminator: + propertyName: enumType + mapping: + CAT: '#/components/schemas/CatWithEnum' + DOG: '#/components/schemas/DogWithEnum' + CatWithEnum: + description: A representation of a cat + type: object + properties: + enumType: + type: string + enum: + - CAT + huntingSkill: + type: string + required: + - enumType + - huntingSkill + DogWithEnum: + description: A representation of a dog + type: object + properties: + enumType: + type: string + enum: + - DOG + packSize: + type: integer + format: int32 + required: + - enumType + - packSize + DogWithAllOf: + description: A representation of a dog + type: object + properties: + typeAllOf: + allOf: + - $ref: '#/components/schemas/PetEnumType' + description: dog type + packSize: + type: integer + format: int32 + required: + - typeAllOf + - packSize + PetNoMapping: + oneOf: + - $ref: '#/components/schemas/CatNoMapping' + - $ref: '#/components/schemas/DogNoMapping' + discriminator: + propertyName: type + CatNoMapping: + description: A representation of a cat + type: object + properties: + type: + type: string + const: CAT + huntingSkill: + type: string + required: + - type + - huntingSkill + DogNoMapping: + description: A representation of a dog + type: object + properties: + type: + type: string + const: DOG + packSize: + type: integer + format: int32 + required: + - type + - packSize \ No newline at end of file