From 2e9ba371d167876811cdd2424feea05b3249627a Mon Sep 17 00:00:00 2001 From: jpfinne Date: Thu, 7 May 2026 21:33:08 +0200 Subject: [PATCH 01/18] wip --- .../openapitools/codegen/DefaultCodegen.java | 167 ++++++++++-------- .../codegen/OpenAPINormalizer.java | 19 ++ .../languages/AbstractJavaCodegen.java | 72 +++++++- .../codegen/languages/SpringCodegen.java | 4 + .../JavaSpring/oneof_interface.mustache | 28 ++- .../assertions/AbstractAnnotationsAssert.java | 4 + .../java/assertions/PropertyAssert.java | 30 ++++ .../java/spring/SpringCodegenTest.java | 42 +++++ 8 files changed, 289 insertions(+), 77 deletions(-) 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 710f7838d72f..62be3754c883 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 @@ -1121,8 +1121,12 @@ public void preprocessOpenAPI(OpenAPI openAPI) { addOneOfNameExtension(s, nOneOf); addOneOfInterfaceModel(s, nOneOf); } else { - // else this is a component schema, so we will just use that as the oneOf interface model - addOneOfNameExtension(s, n); + if (ModelUtils.hasProperties(s) && ModelUtils.hasProperties(s)) { + preprocessOneOfWithProperties(s, n); + } else { + // else this is a component schema, so we will just use that as the oneOf interface model + addOneOfNameExtension(s, n); + } } } else if (ModelUtils.isArraySchema(s)) { Schema items = ModelUtils.getSchemaItems(s); @@ -1141,6 +1145,10 @@ public void preprocessOpenAPI(OpenAPI openAPI) { } } + protected void preprocessOneOfWithProperties(Schema s, String schemaName) { + addOneOfNameExtension(s, schemaName); + } + // override with any special handling of the entire OpenAPI spec document @Override @SuppressWarnings("unused") @@ -2710,13 +2718,23 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map allProperties = new LinkedHashMap<>(); List allRequired = new ArrayList<>(); + boolean skipOneOf = false; // if schema has properties outside of allOf/oneOf/anyOf also add them to m if (ModelUtils.hasProperties(composed)) { - if (ModelUtils.hasOneOf(composed)) { + addVars(m, unaliasPropertySchema(composed.getProperties()), composed.getRequired(), null, null); + if (ModelUtils.hasOneOf(composed) && composed.getDiscriminator() == null && useUnwrapped()) { + skipOneOf = true; + ComposedSchema oneOf = new ComposedSchema(); + oneOf.oneOf(composed.getOneOf()); + composed.oneOf(null); + + String oneOfName = (String)vendorExtensions.get("x-one-of-name"); + oneOfName = oneOfName != null ? "oneOf"+ oneOfName: "oneOf"; + addVars(m, Map.of(oneOfName, oneOf), List.of(), null, null); + } else { LOGGER.warn("'oneOf' is intended to include only the additional optional OAS extension discriminator object. " + "For more details, see https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.9.2.1.3 and the OAS section on 'Composition and Inheritance'."); } - addVars(m, unaliasPropertySchema(composed.getProperties()), composed.getRequired(), null, null); } // parent model @@ -2754,85 +2772,88 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map interfaces = ModelUtils.getInterfaces(composed); - if (!interfaces.isEmpty()) { - // m.interfaces is for backward compatibility - if (m.interfaces == null) - m.interfaces = new ArrayList<>(); + List interfaces = List.of(); + if (!skipOneOf) { + // interfaces (schemas defined in allOf, anyOf, oneOf) + interfaces = ModelUtils.getInterfaces(composed); + if (!interfaces.isEmpty()) { + // m.interfaces is for backward compatibility + if (m.interfaces == null) + m.interfaces = new ArrayList<>(); - for (Schema interfaceSchema : interfaces) { - interfaceSchema = unaliasSchema(interfaceSchema); + for (Schema interfaceSchema : interfaces) { + interfaceSchema = unaliasSchema(interfaceSchema); + + if (StringUtils.isBlank(interfaceSchema.get$ref())) { + // primitive type + String languageType = getTypeDeclaration(interfaceSchema); + CodegenProperty interfaceProperty = fromProperty(languageType, interfaceSchema, false); + if (ModelUtils.isArraySchema(interfaceSchema) || ModelUtils.isMapSchema(interfaceSchema)) { + while (interfaceProperty != null) { + addImport(m, interfaceProperty.complexType); + interfaceProperty = interfaceProperty.items; + } + } - if (StringUtils.isBlank(interfaceSchema.get$ref())) { - // primitive type - String languageType = getTypeDeclaration(interfaceSchema); - CodegenProperty interfaceProperty = fromProperty(languageType, interfaceSchema, false); - if (ModelUtils.isArraySchema(interfaceSchema) || ModelUtils.isMapSchema(interfaceSchema)) { - while (interfaceProperty != null) { - addImport(m, interfaceProperty.complexType); - interfaceProperty = interfaceProperty.items; + if (composed.getAnyOf() != null) { + if (m.anyOf.contains(languageType)) { + LOGGER.debug("{} (anyOf schema) already has `{}` defined and therefore it's skipped.", m.name, languageType); + } else { + m.anyOf.add(languageType); + } + } else if (composed.getOneOf() != null) { + if (m.oneOf.contains(languageType)) { + LOGGER.debug("{} (oneOf schema) already has `{}` defined and therefore it's skipped.", m.name, languageType); + } else { + m.oneOf.add(languageType); + } + } else if (composed.getAllOf() != null) { + // no need to add primitive type to allOf, which should comprise of schemas (models) only + } else { + LOGGER.error("Composed schema has incorrect anyOf, allOf, oneOf defined: {}", composed); } + continue; } - if (composed.getAnyOf() != null) { - if (m.anyOf.contains(languageType)) { - LOGGER.debug("{} (anyOf schema) already has `{}` defined and therefore it's skipped.", m.name, languageType); + // the rest of the section is for model + Schema refSchema = null; + String ref = ModelUtils.getSimpleRef(interfaceSchema.get$ref()); + if (allDefinitions != null) { + refSchema = allDefinitions.get(ref); + } + final String modelName = toModelName(ref); + CodegenProperty interfaceProperty = fromProperty(modelName, interfaceSchema, false); + m.interfaces.add(modelName); + addImport(composed, refSchema, m, modelName); + + if (allDefinitions != null && refSchema != null) { + if (allParents.contains(ref) && supportsMultipleInheritance) { + // multiple inheritance + addProperties(allProperties, allRequired, refSchema, new HashSet<>()); + } else if (parentName != null && parentName.equals(ref) && supportsInheritance) { + // single inheritance + addProperties(allProperties, allRequired, refSchema, new HashSet<>()); } else { - m.anyOf.add(languageType); + // composition + Map newProperties = new LinkedHashMap<>(); + addProperties(newProperties, required, refSchema, new HashSet<>()); + mergeProperties(properties, newProperties); + addProperties(allProperties, allRequired, refSchema, new HashSet<>()); } + } + + if (composed.getAnyOf() != null) { + m.anyOf.add(modelName); } else if (composed.getOneOf() != null) { - if (m.oneOf.contains(languageType)) { - LOGGER.debug("{} (oneOf schema) already has `{}` defined and therefore it's skipped.", m.name, languageType); - } else { - m.oneOf.add(languageType); + m.oneOf.add(modelName); + if (!m.permits.contains(modelName)) { + m.permits.add(modelName); } } else if (composed.getAllOf() != null) { - // no need to add primitive type to allOf, which should comprise of schemas (models) only + m.allOf.add(modelName); } else { LOGGER.error("Composed schema has incorrect anyOf, allOf, oneOf defined: {}", composed); } - continue; - } - - // the rest of the section is for model - Schema refSchema = null; - String ref = ModelUtils.getSimpleRef(interfaceSchema.get$ref()); - if (allDefinitions != null) { - refSchema = allDefinitions.get(ref); - } - final String modelName = toModelName(ref); - CodegenProperty interfaceProperty = fromProperty(modelName, interfaceSchema, false); - m.interfaces.add(modelName); - addImport(composed, refSchema, m, modelName); - - if (allDefinitions != null && refSchema != null) { - if (allParents.contains(ref) && supportsMultipleInheritance) { - // multiple inheritance - addProperties(allProperties, allRequired, refSchema, new HashSet<>()); - } else if (parentName != null && parentName.equals(ref) && supportsInheritance) { - // single inheritance - addProperties(allProperties, allRequired, refSchema, new HashSet<>()); - } else { - // composition - Map newProperties = new LinkedHashMap<>(); - addProperties(newProperties, required, refSchema, new HashSet<>()); - mergeProperties(properties, newProperties); - addProperties(allProperties, allRequired, refSchema, new HashSet<>()); - } - } - - if (composed.getAnyOf() != null) { - m.anyOf.add(modelName); - } else if (composed.getOneOf() != null) { - m.oneOf.add(modelName); - if (!m.permits.contains(modelName)) { - m.permits.add(modelName); - } - } else if (composed.getAllOf() != null) { - m.allOf.add(modelName); - } else { - LOGGER.error("Composed schema has incorrect anyOf, allOf, oneOf defined: {}", composed); } } } @@ -2895,8 +2916,14 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map inputRules) { ruleNames.add(REMOVE_PROPERTIES_FROM_TYPE_OTHER_THAN_OBJECT); ruleNames.add(SORT_MODEL_PROPERTIES); ruleNames.add(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING); + ruleNames.add(USE_UNWRAPPED_FOR_INLINE_ONEOF); // rules that are default to true rules.put(SIMPLIFY_ONEOF_ANYOF, true); @@ -1062,6 +1064,8 @@ protected Schema normalizeOneOf(Schema schema, Set visitedSchemas) { // simplify first as the schema may no longer be a oneOf after processing the rule below schema = processSimplifyOneOf(schema); + schema = processUnwrappedOneOf(schema); + // if it's still a oneOf, loop through the sub-schemas if (schema.getOneOf() != null) { for (int i = 0; i < schema.getOneOf().size(); i++) { @@ -1087,6 +1091,21 @@ protected Schema normalizeOneOf(Schema schema, Set visitedSchemas) { return schema; } + protected Schema processUnwrappedOneOf(Schema schema) { + if (!getRule(USE_UNWRAPPED_FOR_INLINE_ONEOF) || !ModelUtils.hasOneOf(schema) || !ModelUtils.hasProperties(schema)) { + return schema; + } + + Schema newSchema = new ComposedSchema(); + newSchema.oneOf(schema.getOneOf()); + newSchema.addExtension("x-unwrapped", true); +// newSchema.addExtension("x-oneof-jsonCreator", true); +// newSchema.addExtension("x-field-extra-a nnotation", "@JsonUnwrapped"); + schema.getProperties().put("oneOf", newSchema); + schema.oneOf(null); + return schema; + } + protected Schema normalizeAnyOf(Schema schema, Set visitedSchemas) { //transform anyOf into enums if needed schema = processSimplifyAnyOfEnum(schema); 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 874b28d0bcca..3e9a5609fab5 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 @@ -30,10 +30,7 @@ import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.PathItem; import io.swagger.v3.oas.models.examples.Example; -import io.swagger.v3.oas.models.media.Content; -import io.swagger.v3.oas.models.media.MediaType; -import io.swagger.v3.oas.models.media.Schema; -import io.swagger.v3.oas.models.media.StringSchema; +import io.swagger.v3.oas.models.media.*; import io.swagger.v3.oas.models.parameters.Parameter; import io.swagger.v3.oas.models.parameters.RequestBody; import io.swagger.v3.oas.models.servers.Server; @@ -642,6 +639,7 @@ public void processOpts() { importMapping.put("JsonIgnore", "com.fasterxml.jackson.annotation.JsonIgnore"); importMapping.put("JsonIgnoreProperties", "com.fasterxml.jackson.annotation.JsonIgnoreProperties"); importMapping.put("JsonInclude", "com.fasterxml.jackson.annotation.JsonInclude"); + importMapping.put("JsonUnwrapped", "com.fasterxml.jackson.annotation.JsonUnwrapped"); if (openApiNullable) { importMapping.put("JsonNullable", "org.openapitools.jackson.nullable.JsonNullable"); } @@ -722,6 +720,37 @@ public Map postProcessAllModels(Map objs) Map allModels = getAllModels(objs); + if (false && useOneOfInterfaces) { + for (Map.Entry entry : objs.entrySet()) { + CodegenModel model = ModelUtils.getModelByName(entry.getKey(), objs); + for (CodegenProperty p : model.getVars()) { + if (p.vendorExtensions.containsKey("x-unwrapped")) { + String dataType = p.getDataType(); + ModelsMap child = objs.values().stream().filter(ms -> ms.getModels().stream().anyMatch(m -> dataType.equals(m.getModel().dataType ))) + .findFirst().orElseThrow(); + child.getImports().add(Map.of("import", this.importMapping.get("JsonNode"))); + child.getImports().add(Map.of("import", this.importMapping.get("JsonMapper"))); +// modelsMap.getImports(); + child.getModels().forEach(m -> m.getModel().vendorExtensions.put("x-oneof-jsonCreator", true)); + ModelsMap modelsMap = entry.getValue(); +// CodegenModel child = ModelUtils.getModelByName(p.getDataType(), objs); + + } + } + } + +// for (CodegenModel model : allModels.values()) { +// for (CodegenProperty p : model.getVars()) { +// if (p.vendorExtensions.containsKey("x-unwrapped")) { +// CodegenModel child = allModels.get(p.getDataType()); +// child.imports.add("JsonNode"); +// child.imports.add("JsonMapper"); +// child.imports.add("JsonCreator"); +// child.getVendorExtensions().put("x-oneof-jsonCreator", true); +// } +// } +// } + } if (!additionalModelTypeAnnotations.isEmpty()) { for (String modelName : objs.keySet()) { Map models = objs.get(modelName); @@ -1985,6 +2014,15 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert if (property.dataType != null && property.dataType.equals(property.name) && property.dataType.toUpperCase(Locale.ROOT).equals(property.name)) { property.name = property.name.toLowerCase(Locale.ROOT); } + if (property.getVendorExtensions().containsKey("x-unwrapped")) { + model.imports.add("JsonUnwrapped"); + property.getVendorExtensions().put("x-field-extra-annotation", "@JsonUnwrapped"); +// property.vendorExtensions.put("x-oneof-jsonCreator", Boolean.TRUE); +// property.getComposedSchemas().getOneOf() +// pro.imports.add("JsonNode"); +// codegenModel.imports.add("JsonMapper"); + + } } @Override @@ -2785,4 +2823,30 @@ protected void addNullableImportForOperation(CodegenOperation codegenOperation) .findAny() .ifPresent(param -> codegenOperation.imports.add("Nullable")); } + + boolean unwrappedOneOf = true; + @Override + protected void preprocessOneOfWithProperties(Schema s, String schemaName) { + if (unwrappedOneOf) { + Schema newSchema = new ComposedSchema(); + newSchema.oneOf(s.getOneOf()); + newSchema.addExtension("x-unwrapped", true); + String nOneOf = toModelName(schemaName + "OneOf"); +// newSchema.setName(nOneOf+"Wrapper"); + String newSchemaName = nOneOf+ "Wrapper"; + openAPI.getComponents().getSchemas().put(newSchemaName, newSchema); + Schema newSchemaRef = new Schema().$ref("#/components/schemas/" + newSchemaName); + s.getProperties().put("oneOf", newSchemaRef); + s.oneOf(null); + addOneOfNameExtension(newSchema, newSchemaName); + addOneOfInterfaceModel(newSchema, newSchemaName); + CodegenModel cm = addOneOfInterfaces.get(addOneOfInterfaces.size() - 1); + cm.vendorExtensions.put("x-oneof-jsonCreator", "true"); + cm.imports.add("JsonNode"); + cm.imports.add("JsonMapper"); + + } else { + super.preprocessOneOfWithProperties(s, schemaName); + } + } } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java index 76e20d09be36..8e0e4455a667 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java @@ -609,6 +609,10 @@ public void processOpts() { importMapping.put("org.springframework.core.io.Resource", "org.springframework.core.io.Resource"); importMapping.put("DateTimeFormat", "org.springframework.format.annotation.DateTimeFormat"); importMapping.put("ParameterObject", "org.springdoc.api.annotations.ParameterObject"); + String jacksonPackage = (String)additionalProperties.get("jacksonPackage"); + importMapping.put("JsonNode", jacksonPackage + ".databind.JsonNode"); + importMapping.put("JsonMapper", jacksonPackage + ".databind.json.JsonMapper"); + if (isUseSpringBoot3() || isUseSpringBoot4()) { importMapping.put("ParameterObject", "org.springdoc.core.annotations.ParameterObject"); } diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/oneof_interface.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/oneof_interface.mustache index 00fca619c748..b881ee562e13 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/oneof_interface.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/oneof_interface.mustache @@ -6,14 +6,14 @@ {{#discriminator}} {{>typeInfoAnnotation}} -{{/discriminator}}{{^discriminator}}{{#useDeductionForOneOfInterfaces}} +{{/discriminator}}{{^discriminator}}{{#useDeductionForOneOfInterfaces}}{{^vendorExtensions.x-oneof-jsonCreator}} @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) @JsonSubTypes({ {{#interfaceModels}} @JsonSubTypes.Type(value = {{classname}}.class){{^-last}}, {{/-last}} {{/interfaceModels}} }) -{{/useDeductionForOneOfInterfaces}}{{#vendorExtensions.x-class-extra-annotation}}{{{vendorExtensions.x-class-extra-annotation}}} +{{/vendorExtensions.x-oneof-jsonCreator}}{{/useDeductionForOneOfInterfaces}}{{#vendorExtensions.x-class-extra-annotation}}{{{vendorExtensions.x-class-extra-annotation}}} {{/vendorExtensions.x-class-extra-annotation}} {{/discriminator}} {{>generatedAnnotation}} @@ -21,5 +21,27 @@ public {{>sealed}}interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {{>permits}}{ {{#discriminator}} public {{propertyType}} {{propertyGetter}}(); - {{/discriminator}} + {{/discriminator}}{{^discriminator}}{{#useDeductionForOneOfInterfaces}}{{/useDeductionForOneOfInterfaces}}{{#vendorExtensions.x-oneof-jsonCreator}} + + static JsonMapper getJsonMapperForJsonCreator() { + return JsonMapper.shared().rebuild() + .addMixIn({{classname}}.class, {{classname}}Mixin.class) + .build(); + } + + @JsonCreator + static {{classname}} valueOf(JsonNode node) { + {{classname}} value = getJsonMapperForJsonCreator().treeToValue(node, {{classname}}.class); + return value; + } + @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) + @JsonSubTypes({ + {{#interfaceModels}} + @JsonSubTypes.Type(value = {{classname}}.class){{^-last}}, {{/-last}} + {{/interfaceModels}} + }) + static interface {{classname}}Mixin { + + } + {{/vendorExtensions.x-oneof-jsonCreator}}{{/discriminator}} } \ No newline at end of file diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/AbstractAnnotationsAssert.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/AbstractAnnotationsAssert.java index 22c1027aef25..5a94fcd0b72b 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/AbstractAnnotationsAssert.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/AbstractAnnotationsAssert.java @@ -14,6 +14,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.regex.Pattern; import java.util.stream.Collectors; @CanIgnoreReturnValue @@ -149,4 +150,7 @@ private boolean containsSpecificAnnotationNameAndAttributes(Node node, String na return false; } + + + } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/PropertyAssert.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/PropertyAssert.java index e598c210a1f6..04b7ff68baf3 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/PropertyAssert.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/PropertyAssert.java @@ -5,6 +5,8 @@ import org.assertj.core.api.ObjectAssert; import org.assertj.core.util.CanIgnoreReturnValue; +import java.util.regex.Pattern; + @CanIgnoreReturnValue public class PropertyAssert extends ObjectAssert { @@ -43,4 +45,32 @@ public PropertyAnnotationsAssert hasAnnotation(String annotationName) { actual.getAnnotations() ).containsWithName(annotationName); } + + /** + * assert that the annotation is not specifed in an import. + * + * @param name classname of the annotation. For example "Nullable" or a full qualified class name like "java.util.List" + */ + public PropertyAssert doesNotImportAnnotation(final String name) { + String pattern = "import\\s+" + + (name.contains(".")?"" : "[\\w.]+\\.") + + Pattern.quote(name) + ";"; + this.toType().fileDoesNotContainPattern(pattern); + return this; + } + + /** + * assert that the annotation is imported. + * + * @param name clasname of the annotation. For example "Nullable" or a full qualified class name like "java.util.List" + */ + public PropertyAssert doesImportAnnotation(final String name) { + String pattern = "import\\s+" + + (name.contains(".")?"" : "[\\w.]+\\.") + + Pattern.quote(name) + ";"; + this.toType().fileContainsPattern(pattern); + return this; + } + + } 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 5bf385895ac6..95b631bba73c 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 @@ -17,6 +17,7 @@ package org.openapitools.codegen.java.spring; +import com.fasterxml.jackson.annotation.JsonUnwrapped; import com.google.common.collect.ImmutableMap; import io.swagger.parser.OpenAPIParser; import io.swagger.v3.oas.models.OpenAPI; @@ -28,6 +29,7 @@ import org.apache.commons.lang3.StringUtils; import org.assertj.core.api.Assertions; import org.assertj.core.api.MapAssert; +import org.jetbrains.kotlin.name.StandardClassIds; import org.openapitools.codegen.*; import org.openapitools.codegen.config.CodegenConfigurator; import org.openapitools.codegen.config.GlobalSettings; @@ -7612,4 +7614,44 @@ void disableDiscriminatorJsonIgnorePropertiesIsTrueThenJsonIgnorePropertiesShoul JavaFileAssert.assertThat(files.get("BaseConfiguration.java")) .assertTypeAnnotations().containsWithName("JsonIgnoreProperties"); } + + @Test + void unwrapped_oneOf_with_inheritance() throws IOException { + final Map files = generateFromContract("src/test/resources/3_0/spring/issue_23635.yaml", SPRING_BOOT, + Map.of(AbstractJavaCodegen.USE_ONE_OF_INTERFACES, true, + USE_DEDUCTION_FOR_ONE_OF_INTERFACES, true, + USE_SPRING_BOOT4, true, + USE_JACKSON_3, true), + codegen -> codegen.addOpenapiNormalizer("USE_UNWRAPPED_FOR_INLINE_ONEOF", "false")); + + JavaFileAssert.assertThat(files.get("Dummy.java")) + .assertProperty("oneOf") + .assertPropertyAnnotations().containsWithName("JsonUnwrapped") + .toProperty().doesImportAnnotation("JsonUnwrapped"); + + JavaFileAssert.assertThat(files.get("DummyOneOf.java")) + .assertTypeAnnotations().doesNotContainWithName("JsonSubTypes").toType() + .fileContains("static interface DummyOneOfMixin", "@JsonCreator") + .hasImports("tools.jackson.databind.JsonNode", "tools.jackson.databind.json.JsonMapper"); + } + + @Test + void unwrapped_oneOf_with_composition() throws IOException { + final Map files = generateFromContract("src/test/resources/3_0/spring/issue_23635.yaml", SPRING_BOOT, + Map.of(AbstractJavaCodegen.USE_ONE_OF_INTERFACES, false, + USE_DEDUCTION_FOR_ONE_OF_INTERFACES, false, + USE_SPRING_BOOT4, true, + USE_JACKSON_3, true), + codegen -> codegen.addOpenapiNormalizer("USE_UNWRAPPED_FOR_INLINE_ONEOF", "true")); + + JavaFileAssert.assertThat(files.get("Dummy.java")) + .assertProperty("oneOf") + .assertPropertyAnnotations().containsWithName("JsonUnwrapped") + .toProperty().doesImportAnnotation("JsonUnwrapped"); + + JavaFileAssert.assertThat(files.get("DummyOneOf.java")) + .assertTypeAnnotations().doesNotContainWithName("JsonSubTypes").toType() + .fileDoesNotContain("static interface DummyOneOfMixin", "@JsonCreator") + .fileContains("String good;", " String bad;"); + } } From a6f92812a63c26014cdf64694fe2960eb4884d16 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Sun, 10 May 2026 20:12:04 +0200 Subject: [PATCH 02/18] useWrapperForMixedOneOf --- .../openapitools/codegen/DefaultCodegen.java | 10 +- .../codegen/OpenAPINormalizer.java | 19 --- .../languages/AbstractJavaCodegen.java | 111 +++++++++--------- .../codegen/languages/JavaClientCodegen.java | 20 ++++ .../codegen/languages/SpringCodegen.java | 16 +++ .../Java/jacksonMixinConfig.mustache | 44 +++++++ .../resources/Java/oneof_interface.mustache | 18 ++- .../JavaSpring/jacksonMixinConfig.mustache | 44 +++++++ .../JavaSpring/oneof_interface.mustache | 16 +-- .../codegen/java/JavaClientCodegenTest.java | 24 ++++ .../java/spring/SpringCodegenTest.java | 62 ++++++---- .../resources/3_0/oneOf_unwrap_mixed.yaml | 75 ++++++++++++ 12 files changed, 344 insertions(+), 115 deletions(-) create mode 100644 modules/openapi-generator/src/main/resources/Java/jacksonMixinConfig.mustache create mode 100644 modules/openapi-generator/src/main/resources/JavaSpring/jacksonMixinConfig.mustache create mode 100644 modules/openapi-generator/src/test/resources/3_0/oneOf_unwrap_mixed.yaml 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 62be3754c883..54b96d794eb7 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 @@ -1117,12 +1117,14 @@ public void preprocessOpenAPI(OpenAPI openAPI) { String nOneOf = toModelName(n + "OneOf"); if (ModelUtils.isComposedSchema(s)) { if (e.getKey().contains("/")) { + if (true) + throw new IllegalStateException("Impossible location"); // if this is property schema, we also need to generate the oneOf interface model addOneOfNameExtension(s, nOneOf); addOneOfInterfaceModel(s, nOneOf); } else { - if (ModelUtils.hasProperties(s) && ModelUtils.hasProperties(s)) { - preprocessOneOfWithProperties(s, n); + if (ModelUtils.hasOneOf(s) && (ModelUtils.hasProperties(s) || ModelUtils.hasAllOf(s))) { + preprocessMixedOneOf(s, n); } else { // else this is a component schema, so we will just use that as the oneOf interface model addOneOfNameExtension(s, n); @@ -1145,7 +1147,9 @@ public void preprocessOpenAPI(OpenAPI openAPI) { } } - protected void preprocessOneOfWithProperties(Schema s, String schemaName) { + // override with any special handling of OneOf mixed with allOf or properties. + protected void preprocessMixedOneOf(Schema s, String schemaName) { + // backward compatible code (probably wrong) addOneOfNameExtension(s, schemaName); } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 7a4c918c3238..418b3c0ff2b5 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -159,7 +159,6 @@ public class OpenAPINormalizer { // when set to true, sort model properties by name to ensure deterministic output final String SORT_MODEL_PROPERTIES = "SORT_MODEL_PROPERTIES"; - final String USE_UNWRAPPED_FOR_INLINE_ONEOF = "USE_UNWRAPPED_FOR_INLINE_ONEOF"; // ============= end of rules ============= /** @@ -220,7 +219,6 @@ public OpenAPINormalizer(OpenAPI openAPI, Map inputRules) { ruleNames.add(REMOVE_PROPERTIES_FROM_TYPE_OTHER_THAN_OBJECT); ruleNames.add(SORT_MODEL_PROPERTIES); ruleNames.add(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING); - ruleNames.add(USE_UNWRAPPED_FOR_INLINE_ONEOF); // rules that are default to true rules.put(SIMPLIFY_ONEOF_ANYOF, true); @@ -1064,8 +1062,6 @@ protected Schema normalizeOneOf(Schema schema, Set visitedSchemas) { // simplify first as the schema may no longer be a oneOf after processing the rule below schema = processSimplifyOneOf(schema); - schema = processUnwrappedOneOf(schema); - // if it's still a oneOf, loop through the sub-schemas if (schema.getOneOf() != null) { for (int i = 0; i < schema.getOneOf().size(); i++) { @@ -1091,21 +1087,6 @@ protected Schema normalizeOneOf(Schema schema, Set visitedSchemas) { return schema; } - protected Schema processUnwrappedOneOf(Schema schema) { - if (!getRule(USE_UNWRAPPED_FOR_INLINE_ONEOF) || !ModelUtils.hasOneOf(schema) || !ModelUtils.hasProperties(schema)) { - return schema; - } - - Schema newSchema = new ComposedSchema(); - newSchema.oneOf(schema.getOneOf()); - newSchema.addExtension("x-unwrapped", true); -// newSchema.addExtension("x-oneof-jsonCreator", true); -// newSchema.addExtension("x-field-extra-a nnotation", "@JsonUnwrapped"); - schema.getProperties().put("oneOf", newSchema); - schema.oneOf(null); - return schema; - } - protected Schema normalizeAnyOf(Schema schema, Set visitedSchemas) { //transform anyOf into enums if needed schema = processSimplifyAnyOfEnum(schema); 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 272df5531f0d..5a596a3cfc75 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 @@ -111,6 +111,7 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code public static final String CAMEL_CASE_DOLLAR_SIGN = "camelCaseDollarSign"; public static final String USE_ONE_OF_INTERFACES = "useOneOfInterfaces"; + public static final String USE_WRAPPER_FOR_MIXED_ONE_OF = "useWrapperForMixedOneOf"; public static final String LOMBOK = "lombok"; public static final String DEFAULT_TEST_FOLDER = "${project.build.directory}/generated-test-sources/openapi"; public static final String GENERATE_CONSTRUCTOR_WITH_ALL_ARGS = "generateConstructorWithAllArgs"; @@ -222,6 +223,8 @@ protected enum ENUM_PROPERTY_NAMING_TYPE {MACRO_CASE, legacy, original} @Setter protected boolean useJspecify; protected JSpecifyNullableLambda jSpecifyNullableLambda; + @Getter @Setter + protected boolean useWrapperForMixedOneOf; private Map schemaKeyToModelNameCache = new HashMap<>(); @@ -340,6 +343,7 @@ public AbstractJavaCodegen() { cliOptions.add(CliOption.newBoolean(CodegenConstants.HIDE_GENERATION_TIMESTAMP, CodegenConstants.HIDE_GENERATION_TIMESTAMP_DESC, this.isHideGenerationTimestamp())); cliOptions.add(CliOption.newBoolean(WITH_XML, "whether to include support for application/xml content type and include XML annotations in the model (works with libraries that provide support for JSON and XML)")); cliOptions.add(CliOption.newBoolean(USE_ONE_OF_INTERFACES, "whether to use a java interface to describe a set of oneOf options, where each option is a class that implements the interface")); + cliOptions.add(CliOption.newBoolean(USE_WRAPPER_FOR_MIXED_ONE_OF, "whether to use @JsonUnwrapped and a Wrapper interface for inline oneOf combined with allOf/properties and without discriminator")); CliOption dateLibrary = new CliOption(DATE_LIBRARY, "Option. Date library to use").defaultValue(this.getDateLibrary()); Map dateOptions = new HashMap<>(); @@ -603,6 +607,7 @@ public void processOpts() { convertPropertyToStringAndWriteBack(IMPLICIT_HEADERS_REGEX, this::setImplicitHeadersRegex); convertPropertyToBooleanAndWriteBack(CAMEL_CASE_DOLLAR_SIGN, this::setCamelCaseDollarSign); convertPropertyToBooleanAndWriteBack(USE_ONE_OF_INTERFACES, this::setUseOneOfInterfaces); + convertPropertyToBooleanAndWriteBack(USE_WRAPPER_FOR_MIXED_ONE_OF, this::setUseWrapperForMixedOneOf); convertPropertyToStringAndWriteBack(CodegenConstants.ENUM_PROPERTY_NAMING, this::setEnumPropertyNaming); convertPropertyToBooleanAndWriteBack(USE_JSPECIFY, this::setUseJspecify); @@ -721,38 +726,6 @@ public Map postProcessAllModels(Map objs) objs = super.updateAllModels(objs); Map allModels = getAllModels(objs); - - if (false && useOneOfInterfaces) { - for (Map.Entry entry : objs.entrySet()) { - CodegenModel model = ModelUtils.getModelByName(entry.getKey(), objs); - for (CodegenProperty p : model.getVars()) { - if (p.vendorExtensions.containsKey("x-unwrapped")) { - String dataType = p.getDataType(); - ModelsMap child = objs.values().stream().filter(ms -> ms.getModels().stream().anyMatch(m -> dataType.equals(m.getModel().dataType ))) - .findFirst().orElseThrow(); - child.getImports().add(Map.of("import", this.importMapping.get("JsonNode"))); - child.getImports().add(Map.of("import", this.importMapping.get("JsonMapper"))); -// modelsMap.getImports(); - child.getModels().forEach(m -> m.getModel().vendorExtensions.put("x-oneof-jsonCreator", true)); - ModelsMap modelsMap = entry.getValue(); -// CodegenModel child = ModelUtils.getModelByName(p.getDataType(), objs); - - } - } - } - -// for (CodegenModel model : allModels.values()) { -// for (CodegenProperty p : model.getVars()) { -// if (p.vendorExtensions.containsKey("x-unwrapped")) { -// CodegenModel child = allModels.get(p.getDataType()); -// child.imports.add("JsonNode"); -// child.imports.add("JsonMapper"); -// child.imports.add("JsonCreator"); -// child.getVendorExtensions().put("x-oneof-jsonCreator", true); -// } -// } -// } - } if (!additionalModelTypeAnnotations.isEmpty()) { for (String modelName : objs.keySet()) { Map models = objs.get(modelName); @@ -2038,14 +2011,9 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert if (property.dataType != null && property.dataType.equals(property.name) && property.dataType.toUpperCase(Locale.ROOT).equals(property.name)) { property.name = property.name.toLowerCase(Locale.ROOT); } - if (property.getVendorExtensions().containsKey("x-unwrapped")) { + if (property.getVendorExtensions().containsKey("x-unwrappedOneOf")) { model.imports.add("JsonUnwrapped"); property.getVendorExtensions().put("x-field-extra-annotation", "@JsonUnwrapped"); -// property.vendorExtensions.put("x-oneof-jsonCreator", Boolean.TRUE); -// property.getComposedSchemas().getOneOf() -// pro.imports.add("JsonNode"); -// codegenModel.imports.add("JsonMapper"); - } } @@ -2848,29 +2816,60 @@ protected void addNullableImportForOperation(CodegenOperation codegenOperation) .ifPresent(param -> codegenOperation.imports.add("Nullable")); } - boolean unwrappedOneOf = true; @Override - protected void preprocessOneOfWithProperties(Schema s, String schemaName) { - if (unwrappedOneOf) { - Schema newSchema = new ComposedSchema(); - newSchema.oneOf(s.getOneOf()); - newSchema.addExtension("x-unwrapped", true); + protected void preprocessMixedOneOf(Schema s, String schemaName) { + + // skip handling of oneOf + properties + discriminator + // TOOD: improve the logic to accept discriminator mappings NOT matching the oneOf elements. + boolean hasDiscriminator = s.getDiscriminator() != null; + if (useWrapperForMixedOneOf && !hasDiscriminator) { + Schema newOneOfSchema = new ComposedSchema(); + newOneOfSchema.oneOf(s.getOneOf()); + newOneOfSchema.addExtension("x-unwrappedOneOf", true); String nOneOf = toModelName(schemaName + "OneOf"); -// newSchema.setName(nOneOf+"Wrapper"); - String newSchemaName = nOneOf+ "Wrapper"; - openAPI.getComponents().getSchemas().put(newSchemaName, newSchema); + String newSchemaName = nOneOf+ "_wrapper"; + openAPI.getComponents().getSchemas().put(newSchemaName, newOneOfSchema); Schema newSchemaRef = new Schema().$ref("#/components/schemas/" + newSchemaName); - s.getProperties().put("oneOf", newSchemaRef); s.oneOf(null); - addOneOfNameExtension(newSchema, newSchemaName); - addOneOfInterfaceModel(newSchema, newSchemaName); - CodegenModel cm = addOneOfInterfaces.get(addOneOfInterfaces.size() - 1); - cm.vendorExtensions.put("x-oneof-jsonCreator", "true"); - cm.imports.add("JsonNode"); - cm.imports.add("JsonMapper"); - + // TODO: configuration of the property name + String propertyName = "oneOf"; + if (ModelUtils.hasProperties(s)) { + s.getProperties().put(propertyName, newSchemaRef); + } else if (ModelUtils.hasAllOf(s)) { + Schema schema = new Schema(); + schema.setProperties(Map.of(propertyName, newSchemaRef)); + s.getAllOf().add(schema); + } } else { - super.preprocessOneOfWithProperties(s, schemaName); + super.preprocessMixedOneOf(s, schemaName); + } + } + + @Override + public Map updateAllModels(Map objs) { + objs = super.updateAllModels(objs); + if (jackson) { + // handling of USE_WRAPPER_FOR_MIXED_ONE_OF + for (ModelsMap obj : objs.values()) { + for (ModelMap mo : obj.getModels()) { + CodegenModel cm = mo.getModel(); + if (cm.getVendorExtensions().containsKey("x-unwrappedOneOf") && cm.getInterfaceModels() != null) { + addOneOfMixinSupport(obj, cm); + } + } + } } + return objs; + } + + /** + * add JsonCreator and mixin interface to the oneOf interface. + */ + protected void addOneOfMixinSupport(ModelsMap obj, CodegenModel cm) { + ((List)vendorExtensions.computeIfAbsent("x-jackson-mixins", s -> new ArrayList<>())) + .add(cm.classname); + + cm.getVendorExtensions().put("x-oneof-jsonCreator", true); + obj.getImports().add(Map.of("import", importMapping.get("JsonNode"))); } } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java index 26f1b6bd3abe..838d28401920 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java @@ -1321,10 +1321,14 @@ public void setCaseInsensitiveResponseHeaders(final Boolean caseInsensitiveRespo protected void applyJackson2Package() { writePropertyBack(JACKSON_PACKAGE, JACKSON2_PACKAGE); + importMapping.put("JsonNode", JACKSON2_PACKAGE + ".databind.JsonNode"); + importMapping.put("JsonMapper", JACKSON2_PACKAGE + ".databind.json.JsonMapper"); } protected void applyJackson3Package() { writePropertyBack(JACKSON_PACKAGE, JACKSON3_PACKAGE); + importMapping.put("JsonNode", JACKSON3_PACKAGE + ".databind.JsonNode"); + importMapping.put("JsonMapper", JACKSON3_PACKAGE + ".databind.json.JsonMapper"); } public void setSerializationLibrary(String serializationLibrary) { @@ -1397,4 +1401,20 @@ protected void applyJspecify() { // override the default pattern for the "find and replace" jSpecifyNullableLambda.setNullableAnnotation("@" + additionalProperties.get(JAVAX_PACKAGE) + ".annotation.Nullable"); } + + @Override + protected void addOneOfMixinSupport(ModelsMap obj, CodegenModel cm) { + super.addOneOfMixinSupport(obj, cm); + + vendorExtensions.put("x-jackson-mixins-mapper", useJackson3? "JsonMapper": "ObjectMapper"); + obj.getImports().add(Map.of("import", invokerPackage + ".JacksonMixinConfig")); + if (!useJackson3) { + obj.getImports().add(Map.of("import", "com.fasterxml.jackson.core.JsonProcessingException")); + } + if (supportingFiles.stream().noneMatch(sf -> "JacksonMixinConfig.java".equals(sf.getDestinationFilename()))) { + supportingFiles.add(new SupportingFile("jacksonMixinConfig.mustache", + (sourceFolder + File.separator + invokerPackage).replace(".", java.io.File.separator), + "JacksonMixinConfig.java")); + } + } } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java index eb762a140856..4be0d63e3048 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java @@ -1559,4 +1559,20 @@ private void addSpringNullableImport(Set imports) { imports.add("Nullable"); } } + + @Override + protected void addOneOfMixinSupport(ModelsMap obj, CodegenModel cm) { + super.addOneOfMixinSupport(obj, cm); + + vendorExtensions.put("x-jackson-mixins-mapper", useJackson3? "JsonMapper": "ObjectMapper"); + obj.getImports().add(Map.of("import", configPackage + ".JacksonMixinConfig")); + if (!useJackson3) { + obj.getImports().add(Map.of("import", "com.fasterxml.jackson.core.JsonProcessingException")); + } + if (supportingFiles.stream().noneMatch(sf -> "JacksonMixinConfig.java".equals(sf.getDestinationFilename()))) { + supportingFiles.add(new SupportingFile("jacksonMixinConfig.mustache", + (sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator), + "JacksonMixinConfig.java")); + } + } } diff --git a/modules/openapi-generator/src/main/resources/Java/jacksonMixinConfig.mustache b/modules/openapi-generator/src/main/resources/Java/jacksonMixinConfig.mustache new file mode 100644 index 000000000000..6a80ee33b896 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/Java/jacksonMixinConfig.mustache @@ -0,0 +1,44 @@ +package {{invokerPackage}}; + +{{^useJackson3}} +import {{jacksonPackage}}.databind.ObjectMapper; +{{/useJackson3}} +import {{jacksonPackage}}.databind.json.JsonMapper; +{{#vendorExtensions.x-jackson-mixins}} +import {{modelPackage}}.{{.}}; +{{/vendorExtensions.x-jackson-mixins}} + +public class JacksonMixinConfig { + + private static volatile {{vendorExtensions.x-jackson-mixins-mapper}} INSTANCE; + + /** + Get the {{vendorExtensions.x-jackson-mixins-mapper}} used by the @JsonCreator in @JsonUnWrapped interfaces. + */ + public static {{vendorExtensions.x-jackson-mixins-mapper}} getMapper() { + if (INSTANCE == null) { + setBuilder({{^useJackson3}}JsonMapper.builder().findAndAddModules(){{/useJackson3}}{{#useJackson3}}JsonMapper.shared().rebuild(){{/useJackson3}}); + } + return INSTANCE; + } + + /** + Initialize the mapper used by the @JsonCreator. +

+ Do not pass the global {{vendorExtensions.x-jackson-mixins-mapper}} as @JsonUnWrapped does not support deserializer. + */ + public static void set{{vendorExtensions.x-jackson-mixins-mapper}}({{vendorExtensions.x-jackson-mixins-mapper}} mapper) { + INSTANCE = mapper; + } + + /** + Initialize the mapper used by the @JsonCreator. +

+ Configure the Mixins + */ + public static void setBuilder(JsonMapper.Builder jsonMapperBuilder) { + INSTANCE = jsonMapperBuilder{{#vendorExtensions.x-jackson-mixins}} + .addMixIn({{.}}.class, {{.}}.{{.}}Mixin.class){{/vendorExtensions.x-jackson-mixins}} + .build(); + } +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/Java/oneof_interface.mustache b/modules/openapi-generator/src/main/resources/Java/oneof_interface.mustache index 7a77b67ae169..71dcb1e5b071 100644 --- a/modules/openapi-generator/src/main/resources/Java/oneof_interface.mustache +++ b/modules/openapi-generator/src/main/resources/Java/oneof_interface.mustache @@ -5,5 +5,21 @@ public {{>sealed}}interface {{classname}} {{#vendorExtensions.x-implements}}{{#-first}}extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{#-last}} {{/-last}}{{/vendorExtensions.x-implements}}{{>permits}}{ {{#discriminator}} public {{propertyType}} {{propertyGetter}}(); - {{/discriminator}} + {{/discriminator}}{{#vendorExtensions.x-oneof-jsonCreator}} + + @JsonCreator + static {{classname}} valueOf(JsonNode node) {{^useJackson3}}throws JsonProcessingException {{/useJackson3}}{ + return JacksonMixinConfig.getMapper().treeToValue(node, {{classname}}.class); + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) + @JsonSubTypes({ + {{#interfaceModels}} + @JsonSubTypes.Type(value = {{classname}}.class){{^-last}}, {{/-last}} + {{/interfaceModels}} + }) + static interface {{classname}}Mixin { + + } + {{/vendorExtensions.x-oneof-jsonCreator}} } diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/jacksonMixinConfig.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/jacksonMixinConfig.mustache new file mode 100644 index 000000000000..19c6de84e57a --- /dev/null +++ b/modules/openapi-generator/src/main/resources/JavaSpring/jacksonMixinConfig.mustache @@ -0,0 +1,44 @@ +package {{configPackage}}; + +{{^useJackson3}} +import {{jacksonPackage}}.databind.ObjectMapper; +{{/useJackson3}} +import {{jacksonPackage}}.databind.json.JsonMapper; +{{#vendorExtensions.x-jackson-mixins}} +import {{modelPackage}}.{{.}}; +{{/vendorExtensions.x-jackson-mixins}} + +public class JacksonMixinConfig { + + private static volatile {{vendorExtensions.x-jackson-mixins-mapper}} INSTANCE; + + /** + Get the {{vendorExtensions.x-jackson-mixins-mapper}} used by the @JsonCreator in @JsonUnWrapped interfaces. + */ + public static {{vendorExtensions.x-jackson-mixins-mapper}} getMapper() { + if (INSTANCE == null) { + setBuilder({{^useJackson3}}JsonMapper.builder().findAndAddModules(){{/useJackson3}}{{#useJackson3}}JsonMapper.shared().rebuild(){{/useJackson3}}); + } + return INSTANCE; + } + + /** + Initialize the mapper used by the @JsonCreator. +

+ Do not pass the global {{vendorExtensions.x-jackson-mixins-mapper}} as @JsonUnWrapped does not support deserializer. + */ + public static void set{{vendorExtensions.x-jackson-mixins-mapper}}({{vendorExtensions.x-jackson-mixins-mapper}} mapper) { + INSTANCE = mapper; + } + + /** + Initialize the mapper used by the @JsonCreator. +

+ Configure the Mixins + */ + public static void setBuilder(JsonMapper.Builder jsonMapperBuilder) { + INSTANCE = jsonMapperBuilder{{#vendorExtensions.x-jackson-mixins}} + .addMixIn({{.}}.class, {{.}}.{{.}}Mixin.class){{/vendorExtensions.x-jackson-mixins}} + .build(); + } +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/oneof_interface.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/oneof_interface.mustache index b881ee562e13..55c2efd82aa3 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/oneof_interface.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/oneof_interface.mustache @@ -21,19 +21,13 @@ public {{>sealed}}interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {{>permits}}{ {{#discriminator}} public {{propertyType}} {{propertyGetter}}(); - {{/discriminator}}{{^discriminator}}{{#useDeductionForOneOfInterfaces}}{{/useDeductionForOneOfInterfaces}}{{#vendorExtensions.x-oneof-jsonCreator}} - - static JsonMapper getJsonMapperForJsonCreator() { - return JsonMapper.shared().rebuild() - .addMixIn({{classname}}.class, {{classname}}Mixin.class) - .build(); - } + {{/discriminator}}{{#vendorExtensions.x-oneof-jsonCreator}} @JsonCreator - static {{classname}} valueOf(JsonNode node) { - {{classname}} value = getJsonMapperForJsonCreator().treeToValue(node, {{classname}}.class); - return value; + static {{classname}} valueOf(JsonNode node) {{^useJackson3}}throws JsonProcessingException {{/useJackson3}}{ + return JacksonMixinConfig.getMapper().treeToValue(node, {{classname}}.class); } + @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) @JsonSubTypes({ {{#interfaceModels}} @@ -43,5 +37,5 @@ public {{>sealed}}interface {{classname}}{{#vendorExtensions.x-implements}}{{#-f static interface {{classname}}Mixin { } - {{/vendorExtensions.x-oneof-jsonCreator}}{{/discriminator}} + {{/vendorExtensions.x-oneof-jsonCreator}} } \ No newline at end of file diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java index 69fb7de07ada..ce04e0f14c19 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java @@ -4452,4 +4452,28 @@ void oneOf_issue_912() { .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "Source.class", "name", "\"source\"")); } + @Test + void unwrapped_oneOf_with_inheritance_sb3() throws IOException { + final Map files = generateFromContract("src/test/resources/3_0/oneOf_unwrap_mixed.yaml", RESTCLIENT, + Map.of(AbstractJavaCodegen.USE_ONE_OF_INTERFACES, true, + USE_WRAPPER_FOR_MIXED_ONE_OF, true, + USE_SPRING_BOOT4, false)); + + JavaFileAssert.assertThat(files.get("Account.java")) + .assertProperty("oneOf") + .doesImportAnnotation("JsonUnwrapped") + .assertPropertyAnnotations().containsWithName("JsonUnwrapped"); + + JavaFileAssert.assertThat(files.get("AccountOneOfWrapper.java")) + .assertTypeAnnotations().doesNotContainWithName("JsonSubTypes").toType() + .fileContains("static interface AccountOneOfWrapperMixin", "@JsonCreator") + .hasImports("com.fasterxml.jackson.databind.JsonNode"); + JavaFileAssert.assertThat(files.get("JacksonMixinConfig.java")) + .fileContains(".addMixIn(AccountOneOfWrapper.class, AccountOneOfWrapper.AccountOneOfWrapperMixin.class)", + ".addMixIn(BankOneOfWrapper.class, BankOneOfWrapper.BankOneOfWrapperMixin.class)") + .hasImports("org.openapitools.client.model.AccountOneOfWrapper", + "org.openapitools.client.model.BankOneOfWrapper", + "com.fasterxml.jackson.databind.ObjectMapper", + "com.fasterxml.jackson.databind.json.JsonMapper"); + } } 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 122258e61bb6..4f0f2633726a 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 @@ -17,7 +17,6 @@ package org.openapitools.codegen.java.spring; -import com.fasterxml.jackson.annotation.JsonUnwrapped; import com.google.common.collect.ImmutableMap; import io.swagger.parser.OpenAPIParser; import io.swagger.v3.oas.models.OpenAPI; @@ -29,7 +28,6 @@ import org.apache.commons.lang3.StringUtils; import org.assertj.core.api.Assertions; import org.assertj.core.api.MapAssert; -import org.jetbrains.kotlin.name.StandardClassIds; import org.openapitools.codegen.*; import org.openapitools.codegen.config.CodegenConfigurator; import org.openapitools.codegen.config.GlobalSettings; @@ -7729,42 +7727,56 @@ void disableDiscriminatorJsonIgnorePropertiesIsTrueThenJsonIgnorePropertiesShoul } @Test - void unwrapped_oneOf_with_inheritance() throws IOException { - final Map files = generateFromContract("src/test/resources/3_0/spring/issue_23635.yaml", SPRING_BOOT, + void unwrapped_oneOf_with_inheritance_sb4() throws IOException { + final Map files = generateFromContract("src/test/resources/3_0/oneOf_unwrap_mixed.yaml", SPRING_BOOT, Map.of(AbstractJavaCodegen.USE_ONE_OF_INTERFACES, true, USE_DEDUCTION_FOR_ONE_OF_INTERFACES, true, + USE_WRAPPER_FOR_MIXED_ONE_OF, true, USE_SPRING_BOOT4, true, - USE_JACKSON_3, true), - codegen -> codegen.addOpenapiNormalizer("USE_UNWRAPPED_FOR_INLINE_ONEOF", "false")); + USE_JACKSON_3, true)); - JavaFileAssert.assertThat(files.get("Dummy.java")) + JavaFileAssert.assertThat(files.get("Account.java")) .assertProperty("oneOf") - .assertPropertyAnnotations().containsWithName("JsonUnwrapped") - .toProperty().doesImportAnnotation("JsonUnwrapped"); + .doesImportAnnotation("JsonUnwrapped") + .assertPropertyAnnotations().containsWithName("JsonUnwrapped"); - JavaFileAssert.assertThat(files.get("DummyOneOf.java")) + JavaFileAssert.assertThat(files.get("AccountOneOfWrapper.java")) .assertTypeAnnotations().doesNotContainWithName("JsonSubTypes").toType() - .fileContains("static interface DummyOneOfMixin", "@JsonCreator") - .hasImports("tools.jackson.databind.JsonNode", "tools.jackson.databind.json.JsonMapper"); + .fileContains("static interface AccountOneOfWrapperMixin", "@JsonCreator") + .hasImports("tools.jackson.databind.JsonNode"); + JavaFileAssert.assertThat(files.get("JacksonMixinConfig.java")) + .fileContains(".addMixIn(AccountOneOfWrapper.class, AccountOneOfWrapper.AccountOneOfWrapperMixin.class)", + ".addMixIn(BankOneOfWrapper.class, BankOneOfWrapper.BankOneOfWrapperMixin.class)") + .hasImports("org.openapitools.model.AccountOneOfWrapper", + "org.openapitools.model.BankOneOfWrapper", + "tools.jackson.databind.json.JsonMapper"); + JavaFileAssert.assertThat(files.get("Other.java")) + .fileDoesNotContain("JsonUnwrapped"); } @Test - void unwrapped_oneOf_with_composition() throws IOException { - final Map files = generateFromContract("src/test/resources/3_0/spring/issue_23635.yaml", SPRING_BOOT, - Map.of(AbstractJavaCodegen.USE_ONE_OF_INTERFACES, false, - USE_DEDUCTION_FOR_ONE_OF_INTERFACES, false, - USE_SPRING_BOOT4, true, - USE_JACKSON_3, true), - codegen -> codegen.addOpenapiNormalizer("USE_UNWRAPPED_FOR_INLINE_ONEOF", "true")); + void unwrapped_oneOf_with_inheritance_sb3() throws IOException { + final Map files = generateFromContract("src/test/resources/3_0/oneOf_unwrap_mixed.yaml", SPRING_BOOT, + Map.of(AbstractJavaCodegen.USE_ONE_OF_INTERFACES, true, + USE_DEDUCTION_FOR_ONE_OF_INTERFACES, true, + USE_WRAPPER_FOR_MIXED_ONE_OF, true, + USE_SPRING_BOOT3, true)); - JavaFileAssert.assertThat(files.get("Dummy.java")) + JavaFileAssert.assertThat(files.get("Account.java")) .assertProperty("oneOf") - .assertPropertyAnnotations().containsWithName("JsonUnwrapped") - .toProperty().doesImportAnnotation("JsonUnwrapped"); + .doesImportAnnotation("JsonUnwrapped") + .assertPropertyAnnotations().containsWithName("JsonUnwrapped"); - JavaFileAssert.assertThat(files.get("DummyOneOf.java")) + JavaFileAssert.assertThat(files.get("AccountOneOfWrapper.java")) .assertTypeAnnotations().doesNotContainWithName("JsonSubTypes").toType() - .fileDoesNotContain("static interface DummyOneOfMixin", "@JsonCreator") - .fileContains("String good;", " String bad;"); + .fileContains("static interface AccountOneOfWrapperMixin", "@JsonCreator") + .hasImports("com.fasterxml.jackson.databind.JsonNode"); + JavaFileAssert.assertThat(files.get("JacksonMixinConfig.java")) + .fileContains(".addMixIn(AccountOneOfWrapper.class, AccountOneOfWrapper.AccountOneOfWrapperMixin.class)", + ".addMixIn(BankOneOfWrapper.class, BankOneOfWrapper.BankOneOfWrapperMixin.class)") + .hasImports("org.openapitools.model.AccountOneOfWrapper", + "org.openapitools.model.BankOneOfWrapper", + "com.fasterxml.jackson.databind.ObjectMapper", + "com.fasterxml.jackson.databind.json.JsonMapper"); } } diff --git a/modules/openapi-generator/src/test/resources/3_0/oneOf_unwrap_mixed.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_unwrap_mixed.yaml new file mode 100644 index 000000000000..f63acd0351db --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/oneOf_unwrap_mixed.yaml @@ -0,0 +1,75 @@ +openapi: 3.0.3 +info: + version: "1.0.0" + title: test inline enum +paths: + /test: + post: + requestBody: + description: request + content: + application/json: + schema: + $ref: '#/components/schemas/Account' + required: true + responses: + 200: + description: ok +components: + schemas: + Account: + type: object + properties: + owner: + type: string + bank: + $ref: '#/components/schemas/Bank' + oneOf: + - properties: + bankNumber: + type: string + bic: + type: string + - properties: + iban: + type: string + Bank: + allOf: + - properties: + name: + type: string + oneOf: + - properties: + countryAlpha2: + type: string + - properties: + countryAlpha3: + type: string + Additional: + oneOf: + - properties: + a: + type: string + - properties: + b: + type: string + + additionalProperties: + type: integer + Other: + properties: + type: + type: string + discriminator: + propertyName: type + oneOf: + - $ref: '#/components/schemas/OtherA' + - $ref: '#/components/schemas/OtherB' + OtherA: + properties: + a: + type: string + OtherB: + properties: + b: + type: string \ No newline at end of file From cd2a1c5975bc85df44715fa289c0db43e6797c9a Mon Sep 17 00:00:00 2001 From: jpfinne Date: Mon, 11 May 2026 15:57:53 +0200 Subject: [PATCH 03/18] useWrapperForMixedOneOf --- .../main/java/org/openapitools/codegen/DefaultCodegen.java | 7 +++++-- .../src/main/resources/Java/oneof_interface.mustache | 3 ++- .../src/main/resources/JavaSpring/oneof_interface.mustache | 3 ++- 3 files changed, 9 insertions(+), 4 deletions(-) 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 54b96d794eb7..dd2d41bd3bff 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 @@ -1117,8 +1117,11 @@ public void preprocessOpenAPI(OpenAPI openAPI) { String nOneOf = toModelName(n + "OneOf"); if (ModelUtils.isComposedSchema(s)) { if (e.getKey().contains("/")) { - if (true) - throw new IllegalStateException("Impossible location"); + // WARNING: this code was introduce in PR #5400. + // it fixed a NPE + // there is no unit test reaching it with oneOf != null + // So most prabably this code can be removed + // if this is property schema, we also need to generate the oneOf interface model addOneOfNameExtension(s, nOneOf); addOneOfInterfaceModel(s, nOneOf); diff --git a/modules/openapi-generator/src/main/resources/Java/oneof_interface.mustache b/modules/openapi-generator/src/main/resources/Java/oneof_interface.mustache index 71dcb1e5b071..65c443ab2664 100644 --- a/modules/openapi-generator/src/main/resources/Java/oneof_interface.mustache +++ b/modules/openapi-generator/src/main/resources/Java/oneof_interface.mustache @@ -5,7 +5,8 @@ public {{>sealed}}interface {{classname}} {{#vendorExtensions.x-implements}}{{#-first}}extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{#-last}} {{/-last}}{{/vendorExtensions.x-implements}}{{>permits}}{ {{#discriminator}} public {{propertyType}} {{propertyGetter}}(); - {{/discriminator}}{{#vendorExtensions.x-oneof-jsonCreator}} + {{/discriminator}} + {{#vendorExtensions.x-oneof-jsonCreator}} @JsonCreator static {{classname}} valueOf(JsonNode node) {{^useJackson3}}throws JsonProcessingException {{/useJackson3}}{ diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/oneof_interface.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/oneof_interface.mustache index 55c2efd82aa3..288b2ef0c625 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/oneof_interface.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/oneof_interface.mustache @@ -21,7 +21,8 @@ public {{>sealed}}interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {{>permits}}{ {{#discriminator}} public {{propertyType}} {{propertyGetter}}(); - {{/discriminator}}{{#vendorExtensions.x-oneof-jsonCreator}} + {{/discriminator}} + {{#vendorExtensions.x-oneof-jsonCreator}} @JsonCreator static {{classname}} valueOf(JsonNode node) {{^useJackson3}}throws JsonProcessingException {{/useJackson3}}{ From c132be4fca713bbe02d2cc8887b8c6462930f76a Mon Sep 17 00:00:00 2001 From: jpfinne Date: Mon, 11 May 2026 19:10:00 +0200 Subject: [PATCH 04/18] regenerate doc --- docs/generators/java-camel.md | 1 + docs/generators/java-microprofile.md | 1 + docs/generators/java.md | 1 + docs/generators/spring.md | 1 + 4 files changed, 4 insertions(+) diff --git a/docs/generators/java-camel.md b/docs/generators/java-camel.md index 42c864eed0f8..0300cfc6586e 100644 --- a/docs/generators/java-camel.md +++ b/docs/generators/java-camel.md @@ -127,6 +127,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |useSpringController|Annotate the generated API as a Spring Controller| |false| |useSwaggerUI|Open the OpenApi specification in swagger-ui. Will also import and configure needed dependencies| |true| |useTags|use tags for creating interface and controller classnames| |false| +|useWrapperForMixedOneOf|whether to use @JsonUnwrapped and a Wrapper interface for inline oneOf combined with allOf/properties and without discriminator| |false| |virtualService|Generates the virtual service. For more details refer - https://github.com/virtualansoftware/virtualan/wiki| |false| |withXml|whether to include support for application/xml content type and include XML annotations in the model (works with libraries that provide support for JSON and XML)| |false| |xImplementsSkip|Ability to choose interfaces that should NOT be implemented in the models despite their presence in vendor extension `x-implements`. Takes a list of fully qualified interface names. Example: yaml `xImplementsSkip: [com.some.pack.WithPhotoUrls]` skips implementing the interface `com.some.pack.WithPhotoUrls` in any schema| |empty list| diff --git a/docs/generators/java-microprofile.md b/docs/generators/java-microprofile.md index 240e822a7f8c..c1295cd27ed5 100644 --- a/docs/generators/java-microprofile.md +++ b/docs/generators/java-microprofile.md @@ -112,6 +112,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |useSpringBoot4|Generate code and provide dependencies for use with Spring Boot 4.x.| |false| |useUnaryInterceptor|If true it will generate ResponseInterceptors using a UnaryOperator. This can be usefull for manipulating the request before it gets passed, for example doing your own decryption| |false| |useVertx5|Whether to use Vert.x 5 syntax.| |false| +|useWrapperForMixedOneOf|whether to use @JsonUnwrapped and a Wrapper interface for inline oneOf combined with allOf/properties and without discriminator| |false| |webclientBlockingOperations|Making all WebClient operations blocking(sync). Note that if on operation 'x-webclient-blocking: false' then such operation won't be sync| |false| |withAWSV4Signature|whether to include AWS v4 signature support (only available for okhttp-gson library)| |false| |withXml|whether to include support for application/xml content type and include XML annotations in the model (works with libraries that provide support for JSON and XML)| |false| diff --git a/docs/generators/java.md b/docs/generators/java.md index b1046f765a78..2a105be3978b 100644 --- a/docs/generators/java.md +++ b/docs/generators/java.md @@ -112,6 +112,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |useSpringBoot4|Generate code and provide dependencies for use with Spring Boot 4.x.| |false| |useUnaryInterceptor|If true it will generate ResponseInterceptors using a UnaryOperator. This can be usefull for manipulating the request before it gets passed, for example doing your own decryption| |false| |useVertx5|Whether to use Vert.x 5 syntax.| |false| +|useWrapperForMixedOneOf|whether to use @JsonUnwrapped and a Wrapper interface for inline oneOf combined with allOf/properties and without discriminator| |false| |webclientBlockingOperations|Making all WebClient operations blocking(sync). Note that if on operation 'x-webclient-blocking: false' then such operation won't be sync| |false| |withAWSV4Signature|whether to include AWS v4 signature support (only available for okhttp-gson library)| |false| |withXml|whether to include support for application/xml content type and include XML annotations in the model (works with libraries that provide support for JSON and XML)| |false| diff --git a/docs/generators/spring.md b/docs/generators/spring.md index e1a4d7771e61..1b77c5b7db6b 100644 --- a/docs/generators/spring.md +++ b/docs/generators/spring.md @@ -120,6 +120,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |useSpringController|Annotate the generated API as a Spring Controller| |false| |useSwaggerUI|Open the OpenApi specification in swagger-ui. Will also import and configure needed dependencies| |true| |useTags|use tags for creating interface and controller classnames| |false| +|useWrapperForMixedOneOf|whether to use @JsonUnwrapped and a Wrapper interface for inline oneOf combined with allOf/properties and without discriminator| |false| |virtualService|Generates the virtual service. For more details refer - https://github.com/virtualansoftware/virtualan/wiki| |false| |withXml|whether to include support for application/xml content type and include XML annotations in the model (works with libraries that provide support for JSON and XML)| |false| |xImplementsSkip|Ability to choose interfaces that should NOT be implemented in the models despite their presence in vendor extension `x-implements`. Takes a list of fully qualified interface names. Example: yaml `xImplementsSkip: [com.some.pack.WithPhotoUrls]` skips implementing the interface `com.some.pack.WithPhotoUrls` in any schema| |empty list| From 32eb11093c19d25467a0368c1f4b6114be583bf0 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Mon, 11 May 2026 19:52:20 +0200 Subject: [PATCH 05/18] regenerate doc --- docs/generators/java-camel.md | 2 +- docs/generators/java-microprofile.md | 2 +- docs/generators/java.md | 2 +- docs/generators/spring.md | 2 +- .../org/openapitools/codegen/languages/AbstractJavaCodegen.java | 1 - .../org/openapitools/codegen/languages/JavaClientCodegen.java | 1 + .../java/org/openapitools/codegen/languages/SpringCodegen.java | 2 ++ 7 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/generators/java-camel.md b/docs/generators/java-camel.md index 0300cfc6586e..11d046aa7d04 100644 --- a/docs/generators/java-camel.md +++ b/docs/generators/java-camel.md @@ -127,7 +127,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |useSpringController|Annotate the generated API as a Spring Controller| |false| |useSwaggerUI|Open the OpenApi specification in swagger-ui. Will also import and configure needed dependencies| |true| |useTags|use tags for creating interface and controller classnames| |false| -|useWrapperForMixedOneOf|whether to use @JsonUnwrapped and a Wrapper interface for inline oneOf combined with allOf/properties and without discriminator| |false| +|useWrapperForMixedOneOf|whether to use jackson @JsonUnwrapped and a Wrapper interface for inline oneOf combined with allOf/properties and without discriminator| |false| |virtualService|Generates the virtual service. For more details refer - https://github.com/virtualansoftware/virtualan/wiki| |false| |withXml|whether to include support for application/xml content type and include XML annotations in the model (works with libraries that provide support for JSON and XML)| |false| |xImplementsSkip|Ability to choose interfaces that should NOT be implemented in the models despite their presence in vendor extension `x-implements`. Takes a list of fully qualified interface names. Example: yaml `xImplementsSkip: [com.some.pack.WithPhotoUrls]` skips implementing the interface `com.some.pack.WithPhotoUrls` in any schema| |empty list| diff --git a/docs/generators/java-microprofile.md b/docs/generators/java-microprofile.md index c1295cd27ed5..01afab8bfec7 100644 --- a/docs/generators/java-microprofile.md +++ b/docs/generators/java-microprofile.md @@ -112,7 +112,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |useSpringBoot4|Generate code and provide dependencies for use with Spring Boot 4.x.| |false| |useUnaryInterceptor|If true it will generate ResponseInterceptors using a UnaryOperator. This can be usefull for manipulating the request before it gets passed, for example doing your own decryption| |false| |useVertx5|Whether to use Vert.x 5 syntax.| |false| -|useWrapperForMixedOneOf|whether to use @JsonUnwrapped and a Wrapper interface for inline oneOf combined with allOf/properties and without discriminator| |false| +|useWrapperForMixedOneOf|whether to use jackson @JsonUnwrapped and a Wrapper interface for inline oneOf combined with allOf/properties and without discriminator| |false| |webclientBlockingOperations|Making all WebClient operations blocking(sync). Note that if on operation 'x-webclient-blocking: false' then such operation won't be sync| |false| |withAWSV4Signature|whether to include AWS v4 signature support (only available for okhttp-gson library)| |false| |withXml|whether to include support for application/xml content type and include XML annotations in the model (works with libraries that provide support for JSON and XML)| |false| diff --git a/docs/generators/java.md b/docs/generators/java.md index 2a105be3978b..f1c1362fb306 100644 --- a/docs/generators/java.md +++ b/docs/generators/java.md @@ -112,7 +112,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |useSpringBoot4|Generate code and provide dependencies for use with Spring Boot 4.x.| |false| |useUnaryInterceptor|If true it will generate ResponseInterceptors using a UnaryOperator. This can be usefull for manipulating the request before it gets passed, for example doing your own decryption| |false| |useVertx5|Whether to use Vert.x 5 syntax.| |false| -|useWrapperForMixedOneOf|whether to use @JsonUnwrapped and a Wrapper interface for inline oneOf combined with allOf/properties and without discriminator| |false| +|useWrapperForMixedOneOf|whether to use jackson @JsonUnwrapped and a Wrapper interface for inline oneOf combined with allOf/properties and without discriminator| |false| |webclientBlockingOperations|Making all WebClient operations blocking(sync). Note that if on operation 'x-webclient-blocking: false' then such operation won't be sync| |false| |withAWSV4Signature|whether to include AWS v4 signature support (only available for okhttp-gson library)| |false| |withXml|whether to include support for application/xml content type and include XML annotations in the model (works with libraries that provide support for JSON and XML)| |false| diff --git a/docs/generators/spring.md b/docs/generators/spring.md index 1b77c5b7db6b..c7438cf5b765 100644 --- a/docs/generators/spring.md +++ b/docs/generators/spring.md @@ -120,7 +120,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |useSpringController|Annotate the generated API as a Spring Controller| |false| |useSwaggerUI|Open the OpenApi specification in swagger-ui. Will also import and configure needed dependencies| |true| |useTags|use tags for creating interface and controller classnames| |false| -|useWrapperForMixedOneOf|whether to use @JsonUnwrapped and a Wrapper interface for inline oneOf combined with allOf/properties and without discriminator| |false| +|useWrapperForMixedOneOf|whether to use jackson @JsonUnwrapped and a Wrapper interface for inline oneOf combined with allOf/properties and without discriminator| |false| |virtualService|Generates the virtual service. For more details refer - https://github.com/virtualansoftware/virtualan/wiki| |false| |withXml|whether to include support for application/xml content type and include XML annotations in the model (works with libraries that provide support for JSON and XML)| |false| |xImplementsSkip|Ability to choose interfaces that should NOT be implemented in the models despite their presence in vendor extension `x-implements`. Takes a list of fully qualified interface names. Example: yaml `xImplementsSkip: [com.some.pack.WithPhotoUrls]` skips implementing the interface `com.some.pack.WithPhotoUrls` in any schema| |empty list| 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 5a596a3cfc75..139fafb02220 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 @@ -343,7 +343,6 @@ public AbstractJavaCodegen() { cliOptions.add(CliOption.newBoolean(CodegenConstants.HIDE_GENERATION_TIMESTAMP, CodegenConstants.HIDE_GENERATION_TIMESTAMP_DESC, this.isHideGenerationTimestamp())); cliOptions.add(CliOption.newBoolean(WITH_XML, "whether to include support for application/xml content type and include XML annotations in the model (works with libraries that provide support for JSON and XML)")); cliOptions.add(CliOption.newBoolean(USE_ONE_OF_INTERFACES, "whether to use a java interface to describe a set of oneOf options, where each option is a class that implements the interface")); - cliOptions.add(CliOption.newBoolean(USE_WRAPPER_FOR_MIXED_ONE_OF, "whether to use @JsonUnwrapped and a Wrapper interface for inline oneOf combined with allOf/properties and without discriminator")); CliOption dateLibrary = new CliOption(DATE_LIBRARY, "Option. Date library to use").defaultValue(this.getDateLibrary()); Map dateOptions = new HashMap<>(); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java index 838d28401920..aab31e0f2a36 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java @@ -285,6 +285,7 @@ public JavaClientCodegen() { cliOptions.add(CliOption.newBoolean(USE_SEALED_ONE_OF_INTERFACES, "Generate the oneOf interfaces as sealed interfaces. Only supported for WebClient and RestClient.", this.useSealedOneOfInterfaces)); cliOptions.add(CliOption.newBoolean(USE_UNARY_INTERCEPTOR, "If true it will generate ResponseInterceptors using a UnaryOperator. This can be usefull for manipulating the request before it gets passed, for example doing your own decryption", this.useUnaryInterceptor)); cliOptions.add(CliOption.newBoolean(USE_JSPECIFY, "Use Jspecify for null checks. Only supported for " + JSPECIFY_SUPPORTED_LIBRARIES, useJspecify)); + cliOptions.add(CliOption.newBoolean(USE_WRAPPER_FOR_MIXED_ONE_OF, "whether to use jackson @JsonUnwrapped and a Wrapper interface for inline oneOf combined with allOf/properties and without discriminator")); supportedLibraries.put(JERSEY2, "HTTP client: Jersey client 2.25.1. JSON processing: Jackson 2.17.1"); supportedLibraries.put(JERSEY3, "HTTP client: Jersey client 3.1.1. JSON processing: Jackson 2.17.1"); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java index 4be0d63e3048..f2cd4cee1683 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java @@ -345,6 +345,8 @@ public SpringCodegen() { .defaultValue("false") ); cliOptions.add(CliOption.newBoolean(USE_JSPECIFY, "Use Jspecify for null checks", useJspecify)); + cliOptions.add(CliOption.newBoolean(USE_WRAPPER_FOR_MIXED_ONE_OF, "whether to use jackson @JsonUnwrapped and a Wrapper interface for inline oneOf combined with allOf/properties and without discriminator")); + supportedLibraries.put(SPRING_BOOT, "Spring-boot Server application."); supportedLibraries.put(SPRING_CLOUD_LIBRARY, "Spring-Cloud-Feign client with Spring-Boot auto-configured settings."); From d38d951fd8712d102b2e9d655170982b3243370a Mon Sep 17 00:00:00 2001 From: jpfinne Date: Mon, 11 May 2026 23:20:21 +0200 Subject: [PATCH 06/18] Support for useOneOfInterfaces=false --- .../openapitools/codegen/DefaultCodegen.java | 81 +++++++++++-------- .../languages/AbstractJavaCodegen.java | 4 +- .../java/spring/SpringCodegenTest.java | 17 ++++ .../resources/3_0/oneOf_unwrap_mixed.yaml | 11 --- 4 files changed, 66 insertions(+), 47 deletions(-) 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 dd2d41bd3bff..f8383aecf580 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 @@ -1060,7 +1060,7 @@ public void postProcessParameter(CodegenParameter parameter) { @Override @SuppressWarnings("unused") public void preprocessOpenAPI(OpenAPI openAPI) { - if (useOneOfInterfaces && openAPI.getComponents() != null) { + if (openAPI.getComponents() != null) { // we process the openapi schema here to find oneOf schemas and create interface models for them Map schemas = new HashMap<>(openAPI.getComponents().getSchemas()); if (schemas == null) { @@ -1110,40 +1110,51 @@ public void preprocessOpenAPI(OpenAPI openAPI) { } schemas.putAll(propertySchemas); - // go through all gathered schemas and add them as interfaces to be created - for (Map.Entry e : schemas.entrySet()) { - String n = toModelName(e.getKey()); - Schema s = e.getValue(); - String nOneOf = toModelName(n + "OneOf"); - if (ModelUtils.isComposedSchema(s)) { - if (e.getKey().contains("/")) { - // WARNING: this code was introduce in PR #5400. - // it fixed a NPE - // there is no unit test reaching it with oneOf != null - // So most prabably this code can be removed - - // if this is property schema, we also need to generate the oneOf interface model - addOneOfNameExtension(s, nOneOf); - addOneOfInterfaceModel(s, nOneOf); - } else { - if (ModelUtils.hasOneOf(s) && (ModelUtils.hasProperties(s) || ModelUtils.hasAllOf(s))) { - preprocessMixedOneOf(s, n); + if (useOneOfInterfaces) { + // go through all gathered schemas and add them as interfaces to be created + for (Map.Entry e : schemas.entrySet()) { + String n = toModelName(e.getKey()); + Schema s = e.getValue(); + String nOneOf = toModelName(n + "OneOf"); + if (ModelUtils.isComposedSchema(s)) { + if (e.getKey().contains("/")) { + // WARNING: this code was introduce in PR #5400. + // it fixed a NPE + // there is no unit test reaching it with oneOf != null + // So most prabably this code can be removed + + // if this is property schema, we also need to generate the oneOf interface model + addOneOfNameExtension(s, nOneOf); + addOneOfInterfaceModel(s, nOneOf); } else { - // else this is a component schema, so we will just use that as the oneOf interface model - addOneOfNameExtension(s, n); + if (ModelUtils.hasOneOf(s) && (ModelUtils.hasProperties(s) || ModelUtils.hasAllOf(s))) { + preprocessMixedOneOf(s, n); + } else { + // else this is a component schema, so we will just use that as the oneOf interface model + addOneOfNameExtension(s, n); + } + } + } else if (ModelUtils.isArraySchema(s)) { + Schema items = ModelUtils.getSchemaItems(s); + if (ModelUtils.isComposedSchema(items)) { + addOneOfNameExtension(items, nOneOf); + addOneOfInterfaceModel(items, nOneOf); + } + } else if (ModelUtils.isMapSchema(s)) { + Schema addProps = ModelUtils.getAdditionalProperties(s); + if (ModelUtils.isComposedSchema(addProps)) { + addOneOfNameExtension(addProps, nOneOf); + addOneOfInterfaceModel(addProps, nOneOf); } } - } else if (ModelUtils.isArraySchema(s)) { - Schema items = ModelUtils.getSchemaItems(s); - if (ModelUtils.isComposedSchema(items)) { - addOneOfNameExtension(items, nOneOf); - addOneOfInterfaceModel(items, nOneOf); - } - } else if (ModelUtils.isMapSchema(s)) { - Schema addProps = ModelUtils.getAdditionalProperties(s); - if (ModelUtils.isComposedSchema(addProps)) { - addOneOfNameExtension(addProps, nOneOf); - addOneOfInterfaceModel(addProps, nOneOf); + } + } else { + // mixed oneof support even if useOneOfInterfaces=false + for (Map.Entry e : schemas.entrySet()) { + String n = toModelName(e.getKey()); + Schema s = e.getValue(); + if (ModelUtils.hasOneOf(s) && (ModelUtils.hasProperties(s) || ModelUtils.hasAllOf(s))) { + preprocessMixedOneOf(s, n); } } } @@ -1152,8 +1163,10 @@ public void preprocessOpenAPI(OpenAPI openAPI) { // override with any special handling of OneOf mixed with allOf or properties. protected void preprocessMixedOneOf(Schema s, String schemaName) { - // backward compatible code (probably wrong) - addOneOfNameExtension(s, schemaName); + if (useOneOfInterfaces) { + // backward compatible code (probably wrong) + addOneOfNameExtension(s, schemaName); + } } // override with any special handling of the entire OpenAPI spec document 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 139fafb02220..7d35995ff267 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 @@ -2847,8 +2847,8 @@ protected void preprocessMixedOneOf(Schema s, String schemaName) { @Override public Map updateAllModels(Map objs) { objs = super.updateAllModels(objs); - if (jackson) { - // handling of USE_WRAPPER_FOR_MIXED_ONE_OF + if (jackson && useOneOfInterfaces) { + // handling of USE_WRAPPER_FOR_MIXED_ONE_OF with inheritance for (ModelsMap obj : objs.values()) { for (ModelMap mo : obj.getModels()) { CodegenModel cm = mo.getModel(); 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 4f0f2633726a..41c3aceef397 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 @@ -7779,4 +7779,21 @@ void unwrapped_oneOf_with_inheritance_sb3() throws IOException { "com.fasterxml.jackson.databind.ObjectMapper", "com.fasterxml.jackson.databind.json.JsonMapper"); } + + @Test + void unwrapped_oneOf_with_composition() throws IOException { + final Map files = generateFromContract("src/test/resources/3_0/oneOf_unwrap_mixed.yaml", SPRING_BOOT, + Map.of(AbstractJavaCodegen.USE_ONE_OF_INTERFACES, false, + USE_DEDUCTION_FOR_ONE_OF_INTERFACES, false, + USE_WRAPPER_FOR_MIXED_ONE_OF, true, + USE_SPRING_BOOT4, true, + USE_JACKSON_3, true)); + JavaFileAssert.assertThat(files.get("Account.java")) + .assertProperty("oneOf") + .doesImportAnnotation("JsonUnwrapped") + .assertPropertyAnnotations().containsWithName("JsonUnwrapped"); + JavaFileAssert.assertThat(files.get("AccountOneOfWrapper.java")) + .assertTypeAnnotations().doesNotContainWithName("JsonSubTypes").toType() + .fileDoesNotContain("AccountOneOfWrapperMixin", "@JsonCreator"); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/oneOf_unwrap_mixed.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_unwrap_mixed.yaml index f63acd0351db..0dea9e220012 100644 --- a/modules/openapi-generator/src/test/resources/3_0/oneOf_unwrap_mixed.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/oneOf_unwrap_mixed.yaml @@ -45,17 +45,6 @@ components: - properties: countryAlpha3: type: string - Additional: - oneOf: - - properties: - a: - type: string - - properties: - b: - type: string - - additionalProperties: - type: integer Other: properties: type: From ceb1af99be6b18d10aa0c163bab38c44084c01c2 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Mon, 11 May 2026 23:37:07 +0200 Subject: [PATCH 07/18] Better backward compatiblity --- .../src/main/java/org/openapitools/codegen/DefaultCodegen.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f8383aecf580..784c9ec54c85 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 @@ -1151,9 +1151,9 @@ public void preprocessOpenAPI(OpenAPI openAPI) { } else { // mixed oneof support even if useOneOfInterfaces=false for (Map.Entry e : schemas.entrySet()) { - String n = toModelName(e.getKey()); Schema s = e.getValue(); if (ModelUtils.hasOneOf(s) && (ModelUtils.hasProperties(s) || ModelUtils.hasAllOf(s))) { + String n = toModelName(e.getKey()); preprocessMixedOneOf(s, n); } } From 536d359b1abf1dd0af30f4eae26b8f4b79e91144 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Mon, 11 May 2026 23:52:53 +0200 Subject: [PATCH 08/18] Avoid NPE --- .../main/java/org/openapitools/codegen/DefaultCodegen.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 784c9ec54c85..f04e77743f97 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 @@ -1060,12 +1060,9 @@ public void postProcessParameter(CodegenParameter parameter) { @Override @SuppressWarnings("unused") public void preprocessOpenAPI(OpenAPI openAPI) { - if (openAPI.getComponents() != null) { + if (openAPI.getComponents() != null && openAPI.getComponents().getSchemas() != null) { // we process the openapi schema here to find oneOf schemas and create interface models for them Map schemas = new HashMap<>(openAPI.getComponents().getSchemas()); - if (schemas == null) { - schemas = new HashMap<>(); - } Map pathItems = openAPI.getPaths(); // we need to add all request and response bodies to processed schemas From 822085e6377afdf18ce82921d33f2ad64163d65e Mon Sep 17 00:00:00 2001 From: jpfinne Date: Tue, 12 May 2026 00:13:01 +0200 Subject: [PATCH 09/18] Better backward compatiblity --- .../main/java/org/openapitools/codegen/DefaultCodegen.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 f04e77743f97..91d2e6af1287 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 @@ -1060,9 +1060,12 @@ public void postProcessParameter(CodegenParameter parameter) { @Override @SuppressWarnings("unused") public void preprocessOpenAPI(OpenAPI openAPI) { - if (openAPI.getComponents() != null && openAPI.getComponents().getSchemas() != null) { + if (openAPI.getComponents() != null) { // we process the openapi schema here to find oneOf schemas and create interface models for them - Map schemas = new HashMap<>(openAPI.getComponents().getSchemas()); + Map schemas = new HashMap<>(); + if (openAPI.getComponents().getSchemas() != null) { + schemas.putAll(openAPI.getComponents().getSchemas()); + } Map pathItems = openAPI.getPaths(); // we need to add all request and response bodies to processed schemas From fd72fd347b120bee0ba8f8980b161497a2abeeb7 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Tue, 12 May 2026 00:13:30 +0200 Subject: [PATCH 10/18] test for interface/normalClass --- .../openapitools/codegen/java/spring/SpringCodegenTest.java | 3 +++ 1 file changed, 3 insertions(+) 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 41c3aceef397..0761bf20b9c4 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 @@ -7741,6 +7741,7 @@ void unwrapped_oneOf_with_inheritance_sb4() throws IOException { .assertPropertyAnnotations().containsWithName("JsonUnwrapped"); JavaFileAssert.assertThat(files.get("AccountOneOfWrapper.java")) + .isInterface() .assertTypeAnnotations().doesNotContainWithName("JsonSubTypes").toType() .fileContains("static interface AccountOneOfWrapperMixin", "@JsonCreator") .hasImports("tools.jackson.databind.JsonNode"); @@ -7768,6 +7769,7 @@ void unwrapped_oneOf_with_inheritance_sb3() throws IOException { .assertPropertyAnnotations().containsWithName("JsonUnwrapped"); JavaFileAssert.assertThat(files.get("AccountOneOfWrapper.java")) + .isInterface() .assertTypeAnnotations().doesNotContainWithName("JsonSubTypes").toType() .fileContains("static interface AccountOneOfWrapperMixin", "@JsonCreator") .hasImports("com.fasterxml.jackson.databind.JsonNode"); @@ -7793,6 +7795,7 @@ void unwrapped_oneOf_with_composition() throws IOException { .doesImportAnnotation("JsonUnwrapped") .assertPropertyAnnotations().containsWithName("JsonUnwrapped"); JavaFileAssert.assertThat(files.get("AccountOneOfWrapper.java")) + .isNormalClass() .assertTypeAnnotations().doesNotContainWithName("JsonSubTypes").toType() .fileDoesNotContain("AccountOneOfWrapperMixin", "@JsonCreator"); } From 2f06df2ddeab8efef15aa00fa7e513ccb363605e Mon Sep 17 00:00:00 2001 From: jpfinne Date: Thu, 14 May 2026 13:47:44 +0200 Subject: [PATCH 11/18] Move the cliOption to the OpenAPINormalizer --- docs/customization.md | 41 +++ docs/generators/java-camel.md | 1 - docs/generators/java-microprofile.md | 1 - docs/generators/java.md | 1 - docs/generators/spring.md | 1 - .../codegen/CodegenConstants.java | 3 + .../openapitools/codegen/DefaultCodegen.java | 239 +++++++----------- .../codegen/OpenAPINormalizer.java | 49 +++- .../languages/AbstractJavaCodegen.java | 91 ++++--- .../codegen/languages/JavaClientCodegen.java | 17 -- .../codegen/languages/SpringCodegen.java | 17 -- .../Java/jacksonMixinConfig.mustache | 21 +- .../JavaSpring/jacksonMixinConfig.mustache | 22 +- .../codegen/java/JavaClientCodegenTest.java | 16 +- .../java/spring/SpringCodegenTest.java | 41 +-- 15 files changed, 289 insertions(+), 272 deletions(-) diff --git a/docs/customization.md b/docs/customization.md index c529eb4bc5f9..d446d63188f4 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -681,6 +681,47 @@ index 6f27abd..146c61c 100644 type: string ``` +- `USE_UNWRAPPED_FOR_INLINE_ONEOF` set to true to unwrap inline oneOf combined with allOf/properties and without discriminator (aka mixed oneOf). Set the vendorExtension X_ONE_OF_UNWRAPPED to be used by generators. For example the java generator annotates the new oneOf property with the jackson `@JsonUnwrapped` annotation + +Example: +``` +java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java -i modules/openapi-generator/src/test/resources/3_0/oneOf_unwrap_mixed.yaml -o /tmp/java/ --openapi-normalizer X_ONE_OF_UNWRAPPED=true +``` + +Here is what the change in the spec looks like: +```diff +diff --git a/api/openapi.yaml b/api/openapi.yaml +index 6f27abd..146c61c 100644 +--- a/api/openapi.yaml ++++ b/api/openapi.yaml +@@ -9,10 +9,10 @@ components: + schemas: + Account: +- oneOf: +- - properties: +- bankNumber: +- type: string +- bic: +- type: string +- - properties: +- iban: +- type: string + properties: + bank: + $ref: '#/components/schemas/Bank' ++ oneOf: ++ X_ONE_OF_UNWRAPPED: true ++ oneOf: ++ - properties: ++ bankNumber: ++ type: string ++ bic: ++ type: string ++ - properties: ++ iban: ++ type: string +``` + - `FILTER` The `FILTER` parameter allows selective inclusion of API operations based on specific criteria. It applies the `x-internal: true` property to operations that do **not** match the specified values, preventing them from being generated. Multiple filters can be separated by a semicolon. diff --git a/docs/generators/java-camel.md b/docs/generators/java-camel.md index 6dd08379f3cc..2ac49f9d6eaf 100644 --- a/docs/generators/java-camel.md +++ b/docs/generators/java-camel.md @@ -127,7 +127,6 @@ These options may be applied as additional-properties (cli) or configOptions (pl |useSpringController|Annotate the generated API as a Spring Controller| |false| |useSwaggerUI|Open the OpenApi specification in swagger-ui. Will also import and configure needed dependencies| |true| |useTags|use tags for creating interface and controller classnames| |false| -|useWrapperForMixedOneOf|whether to use jackson @JsonUnwrapped and a Wrapper interface for inline oneOf combined with allOf/properties and without discriminator| |false| |virtualService|Generates the virtual service. For more details refer - https://github.com/virtualansoftware/virtualan/wiki| |false| |withXml|whether to include support for application/xml content type and include XML annotations in the model (works with libraries that provide support for JSON and XML)| |false| |xImplementsSkip|Ability to choose interfaces that should NOT be implemented in the models despite their presence in vendor extension `x-implements`. Takes a list of fully qualified interface names. Example: yaml `xImplementsSkip: [com.some.pack.WithPhotoUrls]` skips implementing the interface `com.some.pack.WithPhotoUrls` in any schema| |empty list| diff --git a/docs/generators/java-microprofile.md b/docs/generators/java-microprofile.md index 6a9ef1584040..1f7f33c0b8b1 100644 --- a/docs/generators/java-microprofile.md +++ b/docs/generators/java-microprofile.md @@ -113,7 +113,6 @@ These options may be applied as additional-properties (cli) or configOptions (pl |useSpringBoot4|Generate code and provide dependencies for use with Spring Boot 4.x.| |false| |useUnaryInterceptor|If true it will generate ResponseInterceptors using a UnaryOperator. This can be usefull for manipulating the request before it gets passed, for example doing your own decryption| |false| |useVertx5|Whether to use Vert.x 5 syntax.| |false| -|useWrapperForMixedOneOf|whether to use jackson @JsonUnwrapped and a Wrapper interface for inline oneOf combined with allOf/properties and without discriminator| |false| |webclientBlockingOperations|Making all WebClient operations blocking(sync). Note that if on operation 'x-webclient-blocking: false' then such operation won't be sync| |false| |withAWSV4Signature|whether to include AWS v4 signature support (only available for okhttp-gson library)| |false| |withXml|whether to include support for application/xml content type and include XML annotations in the model (works with libraries that provide support for JSON and XML)| |false| diff --git a/docs/generators/java.md b/docs/generators/java.md index ccd02e2d1e18..f8a8c5a74019 100644 --- a/docs/generators/java.md +++ b/docs/generators/java.md @@ -113,7 +113,6 @@ These options may be applied as additional-properties (cli) or configOptions (pl |useSpringBoot4|Generate code and provide dependencies for use with Spring Boot 4.x.| |false| |useUnaryInterceptor|If true it will generate ResponseInterceptors using a UnaryOperator. This can be usefull for manipulating the request before it gets passed, for example doing your own decryption| |false| |useVertx5|Whether to use Vert.x 5 syntax.| |false| -|useWrapperForMixedOneOf|whether to use jackson @JsonUnwrapped and a Wrapper interface for inline oneOf combined with allOf/properties and without discriminator| |false| |webclientBlockingOperations|Making all WebClient operations blocking(sync). Note that if on operation 'x-webclient-blocking: false' then such operation won't be sync| |false| |withAWSV4Signature|whether to include AWS v4 signature support (only available for okhttp-gson library)| |false| |withXml|whether to include support for application/xml content type and include XML annotations in the model (works with libraries that provide support for JSON and XML)| |false| diff --git a/docs/generators/spring.md b/docs/generators/spring.md index 6a3d29eef8d9..0542bced8084 100644 --- a/docs/generators/spring.md +++ b/docs/generators/spring.md @@ -120,7 +120,6 @@ These options may be applied as additional-properties (cli) or configOptions (pl |useSpringController|Annotate the generated API as a Spring Controller| |false| |useSwaggerUI|Open the OpenApi specification in swagger-ui. Will also import and configure needed dependencies| |true| |useTags|use tags for creating interface and controller classnames| |false| -|useWrapperForMixedOneOf|whether to use jackson @JsonUnwrapped and a Wrapper interface for inline oneOf combined with allOf/properties and without discriminator| |false| |virtualService|Generates the virtual service. For more details refer - https://github.com/virtualansoftware/virtualan/wiki| |false| |withXml|whether to include support for application/xml content type and include XML annotations in the model (works with libraries that provide support for JSON and XML)| |false| |xImplementsSkip|Ability to choose interfaces that should NOT be implemented in the models despite their presence in vendor extension `x-implements`. Takes a list of fully qualified interface names. Example: yaml `xImplementsSkip: [com.some.pack.WithPhotoUrls]` skips implementing the interface `com.some.pack.WithPhotoUrls` in any schema| |empty list| diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java index 7dd1947471c1..2a8184f5e3aa 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java @@ -499,4 +499,7 @@ public static enum ENUM_PROPERTY_NAMING_TYPE {camelCase, PascalCase, snake_case, public static final String X_DISCRIMINATOR_VALUE = "x-discriminator-value"; public static final String X_ONE_OF_NAME = "x-one-of-name"; public static final String X_NULLABLE = "x-nullable"; + + public static final String X_ONE_OF_JSON_CREATOR = "x-oneof-jsonCreator"; + public static final String X_ONE_OF_UNWRAPPED = "x-oneOfunwrapper"; } 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 91d2e6af1287..710f7838d72f 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 @@ -1060,11 +1060,11 @@ public void postProcessParameter(CodegenParameter parameter) { @Override @SuppressWarnings("unused") public void preprocessOpenAPI(OpenAPI openAPI) { - if (openAPI.getComponents() != null) { + if (useOneOfInterfaces && openAPI.getComponents() != null) { // we process the openapi schema here to find oneOf schemas and create interface models for them - Map schemas = new HashMap<>(); - if (openAPI.getComponents().getSchemas() != null) { - schemas.putAll(openAPI.getComponents().getSchemas()); + Map schemas = new HashMap<>(openAPI.getComponents().getSchemas()); + if (schemas == null) { + schemas = new HashMap<>(); } Map pathItems = openAPI.getPaths(); @@ -1110,65 +1110,37 @@ public void preprocessOpenAPI(OpenAPI openAPI) { } schemas.putAll(propertySchemas); - if (useOneOfInterfaces) { - // go through all gathered schemas and add them as interfaces to be created - for (Map.Entry e : schemas.entrySet()) { - String n = toModelName(e.getKey()); - Schema s = e.getValue(); - String nOneOf = toModelName(n + "OneOf"); - if (ModelUtils.isComposedSchema(s)) { - if (e.getKey().contains("/")) { - // WARNING: this code was introduce in PR #5400. - // it fixed a NPE - // there is no unit test reaching it with oneOf != null - // So most prabably this code can be removed - - // if this is property schema, we also need to generate the oneOf interface model - addOneOfNameExtension(s, nOneOf); - addOneOfInterfaceModel(s, nOneOf); - } else { - if (ModelUtils.hasOneOf(s) && (ModelUtils.hasProperties(s) || ModelUtils.hasAllOf(s))) { - preprocessMixedOneOf(s, n); - } else { - // else this is a component schema, so we will just use that as the oneOf interface model - addOneOfNameExtension(s, n); - } - } - } else if (ModelUtils.isArraySchema(s)) { - Schema items = ModelUtils.getSchemaItems(s); - if (ModelUtils.isComposedSchema(items)) { - addOneOfNameExtension(items, nOneOf); - addOneOfInterfaceModel(items, nOneOf); - } - } else if (ModelUtils.isMapSchema(s)) { - Schema addProps = ModelUtils.getAdditionalProperties(s); - if (ModelUtils.isComposedSchema(addProps)) { - addOneOfNameExtension(addProps, nOneOf); - addOneOfInterfaceModel(addProps, nOneOf); - } + // go through all gathered schemas and add them as interfaces to be created + for (Map.Entry e : schemas.entrySet()) { + String n = toModelName(e.getKey()); + Schema s = e.getValue(); + String nOneOf = toModelName(n + "OneOf"); + if (ModelUtils.isComposedSchema(s)) { + if (e.getKey().contains("/")) { + // if this is property schema, we also need to generate the oneOf interface model + addOneOfNameExtension(s, nOneOf); + addOneOfInterfaceModel(s, nOneOf); + } else { + // else this is a component schema, so we will just use that as the oneOf interface model + addOneOfNameExtension(s, n); } - } - } else { - // mixed oneof support even if useOneOfInterfaces=false - for (Map.Entry e : schemas.entrySet()) { - Schema s = e.getValue(); - if (ModelUtils.hasOneOf(s) && (ModelUtils.hasProperties(s) || ModelUtils.hasAllOf(s))) { - String n = toModelName(e.getKey()); - preprocessMixedOneOf(s, n); + } else if (ModelUtils.isArraySchema(s)) { + Schema items = ModelUtils.getSchemaItems(s); + if (ModelUtils.isComposedSchema(items)) { + addOneOfNameExtension(items, nOneOf); + addOneOfInterfaceModel(items, nOneOf); + } + } else if (ModelUtils.isMapSchema(s)) { + Schema addProps = ModelUtils.getAdditionalProperties(s); + if (ModelUtils.isComposedSchema(addProps)) { + addOneOfNameExtension(addProps, nOneOf); + addOneOfInterfaceModel(addProps, nOneOf); } } } } } - // override with any special handling of OneOf mixed with allOf or properties. - protected void preprocessMixedOneOf(Schema s, String schemaName) { - if (useOneOfInterfaces) { - // backward compatible code (probably wrong) - addOneOfNameExtension(s, schemaName); - } - } - // override with any special handling of the entire OpenAPI spec document @Override @SuppressWarnings("unused") @@ -2738,23 +2710,13 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map allProperties = new LinkedHashMap<>(); List allRequired = new ArrayList<>(); - boolean skipOneOf = false; // if schema has properties outside of allOf/oneOf/anyOf also add them to m if (ModelUtils.hasProperties(composed)) { - addVars(m, unaliasPropertySchema(composed.getProperties()), composed.getRequired(), null, null); - if (ModelUtils.hasOneOf(composed) && composed.getDiscriminator() == null && useUnwrapped()) { - skipOneOf = true; - ComposedSchema oneOf = new ComposedSchema(); - oneOf.oneOf(composed.getOneOf()); - composed.oneOf(null); - - String oneOfName = (String)vendorExtensions.get("x-one-of-name"); - oneOfName = oneOfName != null ? "oneOf"+ oneOfName: "oneOf"; - addVars(m, Map.of(oneOfName, oneOf), List.of(), null, null); - } else { + if (ModelUtils.hasOneOf(composed)) { LOGGER.warn("'oneOf' is intended to include only the additional optional OAS extension discriminator object. " + "For more details, see https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.9.2.1.3 and the OAS section on 'Composition and Inheritance'."); } + addVars(m, unaliasPropertySchema(composed.getProperties()), composed.getRequired(), null, null); } // parent model @@ -2792,88 +2754,85 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map interfaces = List.of(); - if (!skipOneOf) { - // interfaces (schemas defined in allOf, anyOf, oneOf) - interfaces = ModelUtils.getInterfaces(composed); - if (!interfaces.isEmpty()) { - // m.interfaces is for backward compatibility - if (m.interfaces == null) - m.interfaces = new ArrayList<>(); + // interfaces (schemas defined in allOf, anyOf, oneOf) + List interfaces = ModelUtils.getInterfaces(composed); + if (!interfaces.isEmpty()) { + // m.interfaces is for backward compatibility + if (m.interfaces == null) + m.interfaces = new ArrayList<>(); - for (Schema interfaceSchema : interfaces) { - interfaceSchema = unaliasSchema(interfaceSchema); + for (Schema interfaceSchema : interfaces) { + interfaceSchema = unaliasSchema(interfaceSchema); - if (StringUtils.isBlank(interfaceSchema.get$ref())) { - // primitive type - String languageType = getTypeDeclaration(interfaceSchema); - CodegenProperty interfaceProperty = fromProperty(languageType, interfaceSchema, false); - if (ModelUtils.isArraySchema(interfaceSchema) || ModelUtils.isMapSchema(interfaceSchema)) { - while (interfaceProperty != null) { - addImport(m, interfaceProperty.complexType); - interfaceProperty = interfaceProperty.items; - } + if (StringUtils.isBlank(interfaceSchema.get$ref())) { + // primitive type + String languageType = getTypeDeclaration(interfaceSchema); + CodegenProperty interfaceProperty = fromProperty(languageType, interfaceSchema, false); + if (ModelUtils.isArraySchema(interfaceSchema) || ModelUtils.isMapSchema(interfaceSchema)) { + while (interfaceProperty != null) { + addImport(m, interfaceProperty.complexType); + interfaceProperty = interfaceProperty.items; } - - if (composed.getAnyOf() != null) { - if (m.anyOf.contains(languageType)) { - LOGGER.debug("{} (anyOf schema) already has `{}` defined and therefore it's skipped.", m.name, languageType); - } else { - m.anyOf.add(languageType); - } - } else if (composed.getOneOf() != null) { - if (m.oneOf.contains(languageType)) { - LOGGER.debug("{} (oneOf schema) already has `{}` defined and therefore it's skipped.", m.name, languageType); - } else { - m.oneOf.add(languageType); - } - } else if (composed.getAllOf() != null) { - // no need to add primitive type to allOf, which should comprise of schemas (models) only - } else { - LOGGER.error("Composed schema has incorrect anyOf, allOf, oneOf defined: {}", composed); - } - continue; } - // the rest of the section is for model - Schema refSchema = null; - String ref = ModelUtils.getSimpleRef(interfaceSchema.get$ref()); - if (allDefinitions != null) { - refSchema = allDefinitions.get(ref); - } - final String modelName = toModelName(ref); - CodegenProperty interfaceProperty = fromProperty(modelName, interfaceSchema, false); - m.interfaces.add(modelName); - addImport(composed, refSchema, m, modelName); - - if (allDefinitions != null && refSchema != null) { - if (allParents.contains(ref) && supportsMultipleInheritance) { - // multiple inheritance - addProperties(allProperties, allRequired, refSchema, new HashSet<>()); - } else if (parentName != null && parentName.equals(ref) && supportsInheritance) { - // single inheritance - addProperties(allProperties, allRequired, refSchema, new HashSet<>()); + if (composed.getAnyOf() != null) { + if (m.anyOf.contains(languageType)) { + LOGGER.debug("{} (anyOf schema) already has `{}` defined and therefore it's skipped.", m.name, languageType); } else { - // composition - Map newProperties = new LinkedHashMap<>(); - addProperties(newProperties, required, refSchema, new HashSet<>()); - mergeProperties(properties, newProperties); - addProperties(allProperties, allRequired, refSchema, new HashSet<>()); + m.anyOf.add(languageType); } - } - - if (composed.getAnyOf() != null) { - m.anyOf.add(modelName); } else if (composed.getOneOf() != null) { - m.oneOf.add(modelName); - if (!m.permits.contains(modelName)) { - m.permits.add(modelName); + if (m.oneOf.contains(languageType)) { + LOGGER.debug("{} (oneOf schema) already has `{}` defined and therefore it's skipped.", m.name, languageType); + } else { + m.oneOf.add(languageType); } } else if (composed.getAllOf() != null) { - m.allOf.add(modelName); + // no need to add primitive type to allOf, which should comprise of schemas (models) only } else { LOGGER.error("Composed schema has incorrect anyOf, allOf, oneOf defined: {}", composed); } + continue; + } + + // the rest of the section is for model + Schema refSchema = null; + String ref = ModelUtils.getSimpleRef(interfaceSchema.get$ref()); + if (allDefinitions != null) { + refSchema = allDefinitions.get(ref); + } + final String modelName = toModelName(ref); + CodegenProperty interfaceProperty = fromProperty(modelName, interfaceSchema, false); + m.interfaces.add(modelName); + addImport(composed, refSchema, m, modelName); + + if (allDefinitions != null && refSchema != null) { + if (allParents.contains(ref) && supportsMultipleInheritance) { + // multiple inheritance + addProperties(allProperties, allRequired, refSchema, new HashSet<>()); + } else if (parentName != null && parentName.equals(ref) && supportsInheritance) { + // single inheritance + addProperties(allProperties, allRequired, refSchema, new HashSet<>()); + } else { + // composition + Map newProperties = new LinkedHashMap<>(); + addProperties(newProperties, required, refSchema, new HashSet<>()); + mergeProperties(properties, newProperties); + addProperties(allProperties, allRequired, refSchema, new HashSet<>()); + } + } + + if (composed.getAnyOf() != null) { + m.anyOf.add(modelName); + } else if (composed.getOneOf() != null) { + m.oneOf.add(modelName); + if (!m.permits.contains(modelName)) { + m.permits.add(modelName); + } + } else if (composed.getAllOf() != null) { + m.allOf.add(modelName); + } else { + LOGGER.error("Composed schema has incorrect anyOf, allOf, oneOf defined: {}", composed); } } } @@ -2936,14 +2895,8 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map inputRules) { ruleNames.add(REMOVE_PROPERTIES_FROM_TYPE_OTHER_THAN_OBJECT); ruleNames.add(SORT_MODEL_PROPERTIES); ruleNames.add(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING); + ruleNames.add(USE_UNWRAPPED_FOR_INLINE_ONEOF); // rules that are default to true rules.put(SIMPLIFY_ONEOF_ANYOF, true); @@ -770,7 +772,7 @@ public Schema normalizeSchema(Schema schema, Set visitedSchemas) { } if (ModelUtils.hasAllOf(schema)) { - return normalizeAllOf(schema, visitedSchemas); + schema = normalizeAllOf(schema, visitedSchemas); } if (ModelUtils.hasOneOf(schema)) { @@ -1079,6 +1081,8 @@ protected Schema normalizeOneOf(Schema schema, Set visitedSchemas) { schema.getOneOf().set(i, normalizeSchema((Schema) item, visitedSchemas)); } schema = processReplaceOneOfByMapping(schema); + schema = processUnwrappedOneOf(schema); + } else { // normalize it as it's no longer an oneOf schema = normalizeSchema(schema, visitedSchemas); @@ -1087,6 +1091,49 @@ protected Schema normalizeOneOf(Schema schema, Set visitedSchemas) { return schema; } + protected Schema processUnwrappedOneOf(Schema schema) { + if (!getRule(USE_UNWRAPPED_FOR_INLINE_ONEOF)) { + return schema; + } + if (!(ModelUtils.hasOneOf(schema) && (ModelUtils.hasProperties(schema) || ModelUtils.hasAllOf(schema)))) { + return schema; + } + + // skip handling of oneOf + properties + discriminator + // accept discriminator mappings NOT matching the oneOf elements. + Discriminator discriminator = schema.getDiscriminator(); + boolean hasDiscriminator = schema.getDiscriminator() != null; + if (hasDiscriminator) { + List oneOfs = schema.getOneOf(); + if (oneOfs.stream().allMatch(oneOf -> oneOf.get$ref() != null)) { + // skip normalization if discriminator but not maping + if (discriminator.getMapping() == null && discriminator.getPropertyName() != null) { + return schema; + } + // if same size for mapping and oneOf, assume that we skip this normalization + if (discriminator.getMapping() != null && discriminator.getMapping().size() == schema.getOneOf().size()) { + return schema; + } + } + } + + Schema newOneOfSchema = new ComposedSchema(); + newOneOfSchema.oneOf(new ArrayList<>(schema.getOneOf())); + newOneOfSchema.addExtension(X_ONE_OF_UNWRAPPED, true); + schema.oneOf(null); + // TODO: configuration of the property name + String propertyName = "oneOf"; + if (ModelUtils.hasProperties(schema)) { + schema.getProperties().put(propertyName, newOneOfSchema); + } else if (ModelUtils.hasAllOf(schema)) { + Schema allOfSchema = new Schema(); + allOfSchema.addProperty(propertyName, newOneOfSchema); + schema.getAllOf().add(allOfSchema); + } + + return schema; + } + protected Schema normalizeAnyOf(Schema schema, Set visitedSchemas) { //transform anyOf into enums if needed schema = processSimplifyAnyOfEnum(schema); 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 e4b669b276ff..3cd9e86fe187 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 @@ -72,8 +72,8 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; -import static org.openapitools.codegen.CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES; -import static org.openapitools.codegen.CodegenConstants.X_IMPLEMENTS; +import static org.openapitools.codegen.CodegenConstants.*; +import static org.openapitools.codegen.VendorExtension.X_FIELD_EXTRA_ANNOTATION; import static org.openapitools.codegen.utils.CamelizeOption.*; import static org.openapitools.codegen.utils.ModelUtils.getSchemaItems; import static org.openapitools.codegen.utils.OnceLogger.once; @@ -112,7 +112,6 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code public static final String CAMEL_CASE_DOLLAR_SIGN = "camelCaseDollarSign"; public static final String USE_ONE_OF_INTERFACES = "useOneOfInterfaces"; - public static final String USE_WRAPPER_FOR_MIXED_ONE_OF = "useWrapperForMixedOneOf"; public static final String LOMBOK = "lombok"; public static final String DEFAULT_TEST_FOLDER = "${project.build.directory}/generated-test-sources/openapi"; public static final String GENERATE_CONSTRUCTOR_WITH_ALL_ARGS = "generateConstructorWithAllArgs"; @@ -609,7 +608,6 @@ public void processOpts() { convertPropertyToStringAndWriteBack(IMPLICIT_HEADERS_REGEX, this::setImplicitHeadersRegex); convertPropertyToBooleanAndWriteBack(CAMEL_CASE_DOLLAR_SIGN, this::setCamelCaseDollarSign); convertPropertyToBooleanAndWriteBack(USE_ONE_OF_INTERFACES, this::setUseOneOfInterfaces); - convertPropertyToBooleanAndWriteBack(USE_WRAPPER_FOR_MIXED_ONE_OF, this::setUseWrapperForMixedOneOf); convertPropertyToStringAndWriteBack(CodegenConstants.ENUM_PROPERTY_NAMING, this::setEnumPropertyNaming); convertPropertyToBooleanAndWriteBack(USE_JSPECIFY, this::setUseJspecify); convertPropertyToBooleanAndWriteBack(USE_DEDUCTION_FOR_ONE_OF_INTERFACES, this::setUseDeductionForOneOfInterfaces); @@ -2015,9 +2013,9 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert if (property.dataType != null && property.dataType.equals(property.name) && property.dataType.toUpperCase(Locale.ROOT).equals(property.name)) { property.name = property.name.toLowerCase(Locale.ROOT); } - if (property.getVendorExtensions().containsKey("x-unwrappedOneOf")) { + if (property.getVendorExtensions().containsKey(X_ONE_OF_UNWRAPPED)) { model.imports.add("JsonUnwrapped"); - property.getVendorExtensions().put("x-field-extra-annotation", "@JsonUnwrapped"); + property.getVendorExtensions().put(X_FIELD_EXTRA_ANNOTATION.getName(), "@JsonUnwrapped"); } } @@ -2674,7 +2672,7 @@ public List getSupportedVendorExtensions() { extensions.add(VendorExtension.X_ACCEPTS); extensions.add(VendorExtension.X_CONTENT_TYPE); extensions.add(VendorExtension.X_CLASS_EXTRA_ANNOTATION); - extensions.add(VendorExtension.X_FIELD_EXTRA_ANNOTATION); + extensions.add(X_FIELD_EXTRA_ANNOTATION); return extensions; } @@ -2820,44 +2818,15 @@ protected void addNullableImportForOperation(CodegenOperation codegenOperation) .ifPresent(param -> codegenOperation.imports.add("Nullable")); } - @Override - protected void preprocessMixedOneOf(Schema s, String schemaName) { - - // skip handling of oneOf + properties + discriminator - // TOOD: improve the logic to accept discriminator mappings NOT matching the oneOf elements. - boolean hasDiscriminator = s.getDiscriminator() != null; - if (useWrapperForMixedOneOf && !hasDiscriminator) { - Schema newOneOfSchema = new ComposedSchema(); - newOneOfSchema.oneOf(s.getOneOf()); - newOneOfSchema.addExtension("x-unwrappedOneOf", true); - String nOneOf = toModelName(schemaName + "OneOf"); - String newSchemaName = nOneOf+ "_wrapper"; - openAPI.getComponents().getSchemas().put(newSchemaName, newOneOfSchema); - Schema newSchemaRef = new Schema().$ref("#/components/schemas/" + newSchemaName); - s.oneOf(null); - // TODO: configuration of the property name - String propertyName = "oneOf"; - if (ModelUtils.hasProperties(s)) { - s.getProperties().put(propertyName, newSchemaRef); - } else if (ModelUtils.hasAllOf(s)) { - Schema schema = new Schema(); - schema.setProperties(Map.of(propertyName, newSchemaRef)); - s.getAllOf().add(schema); - } - } else { - super.preprocessMixedOneOf(s, schemaName); - } - } - @Override public Map updateAllModels(Map objs) { objs = super.updateAllModels(objs); if (jackson && useOneOfInterfaces) { - // handling of USE_WRAPPER_FOR_MIXED_ONE_OF with inheritance + // handling of X_ONE_OF_UNWRAPPED with inheritance for (ModelsMap obj : objs.values()) { for (ModelMap mo : obj.getModels()) { CodegenModel cm = mo.getModel(); - if (cm.getVendorExtensions().containsKey("x-unwrappedOneOf") && cm.getInterfaceModels() != null) { + if (cm.getVendorExtensions().containsKey(X_ONE_OF_UNWRAPPED) && cm.getInterfaceModels() != null) { addOneOfMixinSupport(obj, cm); } } @@ -2867,13 +2836,51 @@ public Map updateAllModels(Map objs) { } /** - * add JsonCreator and mixin interface to the oneOf interface. + * Add JsonCreator and mixin interface to the oneOf interface. + *

+ * Add the necessary imports. + *

+ * Construct a vendorExtension X_ONE_OF_JSON_CREATOR with a map containing: + *

    + *
  1. >mapper: JsonMapper or ObjectMapper + *
  2. mixins: a list with the class name of the oneOf classes + *
*/ protected void addOneOfMixinSupport(ModelsMap obj, CodegenModel cm) { - ((List)vendorExtensions.computeIfAbsent("x-jackson-mixins", s -> new ArrayList<>())) - .add(cm.classname); + String configPackage = getConfigPackage(); + Map config; + if (supportingFiles.stream().noneMatch(sf -> "JacksonMixinConfig.java".equals(sf.getDestinationFilename()))) { + supportingFiles.add(new SupportingFile("jacksonMixinConfig.mustache", + (sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator), + "JacksonMixinConfig.java")); + config = Map.of( + "mapper", isUseJackson3() ? "JsonMapper" : "ObjectMapper", + "mixins", new ArrayList<>()); + vendorExtensions.put("x-jacksonMixinConfig", config); + } else { + config = ( Map)vendorExtensions.get("x-jacksonMixinConfig"); + } + cm.vendorExtensions.put(X_ONE_OF_JSON_CREATOR, config); + ((List)config.get("mixins")).add(cm.classname); - cm.getVendorExtensions().put("x-oneof-jsonCreator", true); + if (!isUseJackson3()) { + obj.getImports().add(Map.of("import", "com.fasterxml.jackson.core.JsonProcessingException")); + } obj.getImports().add(Map.of("import", importMapping.get("JsonNode"))); + obj.getImports().add(Map.of("import", configPackage + ".JacksonMixinConfig")); + + } + + /* + * return the config package. + * + * by default use invokerPackage + */ + protected String getConfigPackage() { + return invokerPackage; + } + + protected boolean isUseJackson3() { + return false; } } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java index 2d1f70d9566b..db47af759e40 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java @@ -285,7 +285,6 @@ public JavaClientCodegen() { cliOptions.add(CliOption.newBoolean(USE_SEALED_ONE_OF_INTERFACES, "Generate the oneOf interfaces as sealed interfaces. Only supported for WebClient and RestClient.", this.useSealedOneOfInterfaces)); cliOptions.add(CliOption.newBoolean(USE_UNARY_INTERCEPTOR, "If true it will generate ResponseInterceptors using a UnaryOperator. This can be usefull for manipulating the request before it gets passed, for example doing your own decryption", this.useUnaryInterceptor)); cliOptions.add(CliOption.newBoolean(USE_JSPECIFY, "Use Jspecify for null checks. Only supported for " + JSPECIFY_SUPPORTED_LIBRARIES, useJspecify)); - cliOptions.add(CliOption.newBoolean(USE_WRAPPER_FOR_MIXED_ONE_OF, "whether to use jackson @JsonUnwrapped and a Wrapper interface for inline oneOf combined with allOf/properties and without discriminator")); cliOptions.add(CliOption.newBoolean(USE_DEDUCTION_FOR_ONE_OF_INTERFACES, USE_DEDUCTION_FOR_ONE_OF_INTERFACES_DESC, useDeductionForOneOfInterfaces)); supportedLibraries.put(JERSEY2, "HTTP client: Jersey client 2.25.1. JSON processing: Jackson 2.17.1"); @@ -1403,20 +1402,4 @@ protected void applyJspecify() { // override the default pattern for the "find and replace" jSpecifyNullableLambda.setNullableAnnotation("@" + additionalProperties.get(JAVAX_PACKAGE) + ".annotation.Nullable"); } - - @Override - protected void addOneOfMixinSupport(ModelsMap obj, CodegenModel cm) { - super.addOneOfMixinSupport(obj, cm); - - vendorExtensions.put("x-jackson-mixins-mapper", useJackson3? "JsonMapper": "ObjectMapper"); - obj.getImports().add(Map.of("import", invokerPackage + ".JacksonMixinConfig")); - if (!useJackson3) { - obj.getImports().add(Map.of("import", "com.fasterxml.jackson.core.JsonProcessingException")); - } - if (supportingFiles.stream().noneMatch(sf -> "JacksonMixinConfig.java".equals(sf.getDestinationFilename()))) { - supportingFiles.add(new SupportingFile("jacksonMixinConfig.mustache", - (sourceFolder + File.separator + invokerPackage).replace(".", java.io.File.separator), - "JacksonMixinConfig.java")); - } - } } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java index 0e1dbbdd7998..317d35fcf680 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java @@ -344,7 +344,6 @@ public SpringCodegen() { .defaultValue("false") ); cliOptions.add(CliOption.newBoolean(USE_JSPECIFY, "Use Jspecify for null checks", useJspecify)); - cliOptions.add(CliOption.newBoolean(USE_WRAPPER_FOR_MIXED_ONE_OF, "whether to use jackson @JsonUnwrapped and a Wrapper interface for inline oneOf combined with allOf/properties and without discriminator")); supportedLibraries.put(SPRING_BOOT, "Spring-boot Server application."); supportedLibraries.put(SPRING_CLOUD_LIBRARY, @@ -1559,20 +1558,4 @@ private void addSpringNullableImport(Set imports) { imports.add("Nullable"); } } - - @Override - protected void addOneOfMixinSupport(ModelsMap obj, CodegenModel cm) { - super.addOneOfMixinSupport(obj, cm); - - vendorExtensions.put("x-jackson-mixins-mapper", useJackson3? "JsonMapper": "ObjectMapper"); - obj.getImports().add(Map.of("import", configPackage + ".JacksonMixinConfig")); - if (!useJackson3) { - obj.getImports().add(Map.of("import", "com.fasterxml.jackson.core.JsonProcessingException")); - } - if (supportingFiles.stream().noneMatch(sf -> "JacksonMixinConfig.java".equals(sf.getDestinationFilename()))) { - supportingFiles.add(new SupportingFile("jacksonMixinConfig.mustache", - (sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator), - "JacksonMixinConfig.java")); - } - } } diff --git a/modules/openapi-generator/src/main/resources/Java/jacksonMixinConfig.mustache b/modules/openapi-generator/src/main/resources/Java/jacksonMixinConfig.mustache index 6a80ee33b896..ecff7f73998d 100644 --- a/modules/openapi-generator/src/main/resources/Java/jacksonMixinConfig.mustache +++ b/modules/openapi-generator/src/main/resources/Java/jacksonMixinConfig.mustache @@ -1,21 +1,21 @@ package {{invokerPackage}}; +{{#vendorExtensions.x-jacksonMixinConfig}} {{^useJackson3}} import {{jacksonPackage}}.databind.ObjectMapper; {{/useJackson3}} import {{jacksonPackage}}.databind.json.JsonMapper; -{{#vendorExtensions.x-jackson-mixins}} +{{#mixins}} import {{modelPackage}}.{{.}}; -{{/vendorExtensions.x-jackson-mixins}} +{{/mixins}} public class JacksonMixinConfig { - - private static volatile {{vendorExtensions.x-jackson-mixins-mapper}} INSTANCE; + private static volatile {{mapper}} INSTANCE; /** - Get the {{vendorExtensions.x-jackson-mixins-mapper}} used by the @JsonCreator in @JsonUnWrapped interfaces. + Get the {{mapper}} used by the @JsonCreator in @JsonUnWrapped interfaces. */ - public static {{vendorExtensions.x-jackson-mixins-mapper}} getMapper() { + public static {{mapper}} getMapper() { if (INSTANCE == null) { setBuilder({{^useJackson3}}JsonMapper.builder().findAndAddModules(){{/useJackson3}}{{#useJackson3}}JsonMapper.shared().rebuild(){{/useJackson3}}); } @@ -25,9 +25,9 @@ public class JacksonMixinConfig { /** Initialize the mapper used by the @JsonCreator.

- Do not pass the global {{vendorExtensions.x-jackson-mixins-mapper}} as @JsonUnWrapped does not support deserializer. + Do not pass the global {{mapper}} as @JsonUnwrapped does not support deserializer. */ - public static void set{{vendorExtensions.x-jackson-mixins-mapper}}({{vendorExtensions.x-jackson-mixins-mapper}} mapper) { + public static void set{{mapper}}({{mapper}} mapper) { INSTANCE = mapper; } @@ -37,8 +37,9 @@ public class JacksonMixinConfig { Configure the Mixins */ public static void setBuilder(JsonMapper.Builder jsonMapperBuilder) { - INSTANCE = jsonMapperBuilder{{#vendorExtensions.x-jackson-mixins}} - .addMixIn({{.}}.class, {{.}}.{{.}}Mixin.class){{/vendorExtensions.x-jackson-mixins}} + INSTANCE = jsonMapperBuilder{{#mixins}} + .addMixIn({{.}}.class, {{.}}.{{.}}Mixin.class){{/mixins}} .build(); } + {{/vendorExtensions.x-jacksonMixinConfig}} } \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/jacksonMixinConfig.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/jacksonMixinConfig.mustache index 19c6de84e57a..d42bd0752be1 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/jacksonMixinConfig.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/jacksonMixinConfig.mustache @@ -1,21 +1,22 @@ package {{configPackage}}; +{{#vendorExtensions.x-jacksonMixinConfig}} {{^useJackson3}} import {{jacksonPackage}}.databind.ObjectMapper; {{/useJackson3}} import {{jacksonPackage}}.databind.json.JsonMapper; -{{#vendorExtensions.x-jackson-mixins}} +{{#mixins}} import {{modelPackage}}.{{.}}; -{{/vendorExtensions.x-jackson-mixins}} +{{/mixins}} public class JacksonMixinConfig { - private static volatile {{vendorExtensions.x-jackson-mixins-mapper}} INSTANCE; + private static volatile {{mapper}} INSTANCE; /** - Get the {{vendorExtensions.x-jackson-mixins-mapper}} used by the @JsonCreator in @JsonUnWrapped interfaces. + Get the {{vmapper}} used by the @JsonCreator in @JsonUnWrapped interfaces. */ - public static {{vendorExtensions.x-jackson-mixins-mapper}} getMapper() { + public static {{mapper}} getMapper() { if (INSTANCE == null) { setBuilder({{^useJackson3}}JsonMapper.builder().findAndAddModules(){{/useJackson3}}{{#useJackson3}}JsonMapper.shared().rebuild(){{/useJackson3}}); } @@ -25,9 +26,9 @@ public class JacksonMixinConfig { /** Initialize the mapper used by the @JsonCreator.

- Do not pass the global {{vendorExtensions.x-jackson-mixins-mapper}} as @JsonUnWrapped does not support deserializer. + Do not pass the global {{mapper}} as @JsonUnwrapped does not support deserializer. */ - public static void set{{vendorExtensions.x-jackson-mixins-mapper}}({{vendorExtensions.x-jackson-mixins-mapper}} mapper) { + public static void set{{mapper}}({{mapper}} mapper) { INSTANCE = mapper; } @@ -37,8 +38,9 @@ public class JacksonMixinConfig { Configure the Mixins */ public static void setBuilder(JsonMapper.Builder jsonMapperBuilder) { - INSTANCE = jsonMapperBuilder{{#vendorExtensions.x-jackson-mixins}} - .addMixIn({{.}}.class, {{.}}.{{.}}Mixin.class){{/vendorExtensions.x-jackson-mixins}} + INSTANCE = jsonMapperBuilder{{#mixins}} + .addMixIn({{.}}.class, {{.}}.{{.}}Mixin.class){{/mixins}} .build(); } -} \ No newline at end of file +} +{{/vendorExtensions.x-jacksonMixinConfig}} \ No newline at end of file diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java index 8adc28bf02fc..c0ccbe22bb89 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java @@ -4456,23 +4456,23 @@ void oneOf_issue_912() { void unwrapped_oneOf_with_inheritance_sb3() throws IOException { final Map files = generateFromContract("src/test/resources/3_0/oneOf_unwrap_mixed.yaml", RESTCLIENT, Map.of(AbstractJavaCodegen.USE_ONE_OF_INTERFACES, true, - USE_WRAPPER_FOR_MIXED_ONE_OF, true, - USE_SPRING_BOOT4, false)); + USE_SPRING_BOOT4, false), + configurator -> configurator.setOpenapiNormalizer(Map.of("USE_UNWRAPPED_FOR_INLINE_ONEOF", "true"))); JavaFileAssert.assertThat(files.get("Account.java")) .assertProperty("oneOf") .doesImportAnnotation("JsonUnwrapped") .assertPropertyAnnotations().containsWithName("JsonUnwrapped"); - JavaFileAssert.assertThat(files.get("AccountOneOfWrapper.java")) + JavaFileAssert.assertThat(files.get("AccountOneOf.java")) .assertTypeAnnotations().doesNotContainWithName("JsonSubTypes").toType() - .fileContains("static interface AccountOneOfWrapperMixin", "@JsonCreator") + .fileContains("static interface AccountOneOfMixin", "@JsonCreator") .hasImports("com.fasterxml.jackson.databind.JsonNode"); JavaFileAssert.assertThat(files.get("JacksonMixinConfig.java")) - .fileContains(".addMixIn(AccountOneOfWrapper.class, AccountOneOfWrapper.AccountOneOfWrapperMixin.class)", - ".addMixIn(BankOneOfWrapper.class, BankOneOfWrapper.BankOneOfWrapperMixin.class)") - .hasImports("org.openapitools.client.model.AccountOneOfWrapper", - "org.openapitools.client.model.BankOneOfWrapper", + .fileContains(".addMixIn(AccountOneOf.class, AccountOneOf.AccountOneOfMixin.class)", + ".addMixIn(BankAllOfOneOf.class, BankAllOfOneOf.BankAllOfOneOfMixin.class)") + .hasImports("org.openapitools.client.model.AccountOneOf", + "org.openapitools.client.model.BankAllOfOneOf", "com.fasterxml.jackson.databind.ObjectMapper", "com.fasterxml.jackson.databind.json.JsonMapper"); } 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 0761bf20b9c4..e04b4b6a6deb 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 @@ -7731,25 +7731,25 @@ void unwrapped_oneOf_with_inheritance_sb4() throws IOException { final Map files = generateFromContract("src/test/resources/3_0/oneOf_unwrap_mixed.yaml", SPRING_BOOT, Map.of(AbstractJavaCodegen.USE_ONE_OF_INTERFACES, true, USE_DEDUCTION_FOR_ONE_OF_INTERFACES, true, - USE_WRAPPER_FOR_MIXED_ONE_OF, true, USE_SPRING_BOOT4, true, - USE_JACKSON_3, true)); + USE_JACKSON_3, true), + configurator -> configurator.setOpenapiNormalizer(Map.of("USE_UNWRAPPED_FOR_INLINE_ONEOF", "true"))); JavaFileAssert.assertThat(files.get("Account.java")) .assertProperty("oneOf") .doesImportAnnotation("JsonUnwrapped") .assertPropertyAnnotations().containsWithName("JsonUnwrapped"); - JavaFileAssert.assertThat(files.get("AccountOneOfWrapper.java")) + JavaFileAssert.assertThat(files.get("AccountOneOf.java")) .isInterface() .assertTypeAnnotations().doesNotContainWithName("JsonSubTypes").toType() - .fileContains("static interface AccountOneOfWrapperMixin", "@JsonCreator") + .fileContains("static interface AccountOneOfMixin", "@JsonCreator") .hasImports("tools.jackson.databind.JsonNode"); JavaFileAssert.assertThat(files.get("JacksonMixinConfig.java")) - .fileContains(".addMixIn(AccountOneOfWrapper.class, AccountOneOfWrapper.AccountOneOfWrapperMixin.class)", - ".addMixIn(BankOneOfWrapper.class, BankOneOfWrapper.BankOneOfWrapperMixin.class)") - .hasImports("org.openapitools.model.AccountOneOfWrapper", - "org.openapitools.model.BankOneOfWrapper", + .fileContains(".addMixIn(AccountOneOf.class, AccountOneOf.AccountOneOfMixin.class)", + ".addMixIn(BankAllOfOneOf.class, BankAllOfOneOf.BankAllOfOneOfMixin.class)") + .hasImports("org.openapitools.model.AccountOneOf", + "org.openapitools.model.BankAllOfOneOf", "tools.jackson.databind.json.JsonMapper"); JavaFileAssert.assertThat(files.get("Other.java")) .fileDoesNotContain("JsonUnwrapped"); @@ -7760,24 +7760,25 @@ void unwrapped_oneOf_with_inheritance_sb3() throws IOException { final Map files = generateFromContract("src/test/resources/3_0/oneOf_unwrap_mixed.yaml", SPRING_BOOT, Map.of(AbstractJavaCodegen.USE_ONE_OF_INTERFACES, true, USE_DEDUCTION_FOR_ONE_OF_INTERFACES, true, - USE_WRAPPER_FOR_MIXED_ONE_OF, true, - USE_SPRING_BOOT3, true)); + USE_SPRING_BOOT3, true) + , + configurator -> configurator.setOpenapiNormalizer(Map.of("USE_UNWRAPPED_FOR_INLINE_ONEOF", "true"))); JavaFileAssert.assertThat(files.get("Account.java")) .assertProperty("oneOf") .doesImportAnnotation("JsonUnwrapped") .assertPropertyAnnotations().containsWithName("JsonUnwrapped"); - JavaFileAssert.assertThat(files.get("AccountOneOfWrapper.java")) + JavaFileAssert.assertThat(files.get("AccountOneOf.java")) .isInterface() .assertTypeAnnotations().doesNotContainWithName("JsonSubTypes").toType() - .fileContains("static interface AccountOneOfWrapperMixin", "@JsonCreator") + .fileContains("static interface AccountOneOfMixin", "@JsonCreator") .hasImports("com.fasterxml.jackson.databind.JsonNode"); JavaFileAssert.assertThat(files.get("JacksonMixinConfig.java")) - .fileContains(".addMixIn(AccountOneOfWrapper.class, AccountOneOfWrapper.AccountOneOfWrapperMixin.class)", - ".addMixIn(BankOneOfWrapper.class, BankOneOfWrapper.BankOneOfWrapperMixin.class)") - .hasImports("org.openapitools.model.AccountOneOfWrapper", - "org.openapitools.model.BankOneOfWrapper", + .fileContains(".addMixIn(AccountOneOf.class, AccountOneOf.AccountOneOfMixin.class)", + ".addMixIn(BankAllOfOneOf.class, BankAllOfOneOf.BankAllOfOneOfMixin.class)") + .hasImports("org.openapitools.model.AccountOneOf", + "org.openapitools.model.BankAllOfOneOf", "com.fasterxml.jackson.databind.ObjectMapper", "com.fasterxml.jackson.databind.json.JsonMapper"); } @@ -7787,16 +7788,16 @@ void unwrapped_oneOf_with_composition() throws IOException { final Map files = generateFromContract("src/test/resources/3_0/oneOf_unwrap_mixed.yaml", SPRING_BOOT, Map.of(AbstractJavaCodegen.USE_ONE_OF_INTERFACES, false, USE_DEDUCTION_FOR_ONE_OF_INTERFACES, false, - USE_WRAPPER_FOR_MIXED_ONE_OF, true, USE_SPRING_BOOT4, true, - USE_JACKSON_3, true)); + USE_JACKSON_3, true), + configurator -> configurator.setOpenapiNormalizer(Map.of("USE_UNWRAPPED_FOR_INLINE_ONEOF", "true"))); JavaFileAssert.assertThat(files.get("Account.java")) .assertProperty("oneOf") .doesImportAnnotation("JsonUnwrapped") .assertPropertyAnnotations().containsWithName("JsonUnwrapped"); - JavaFileAssert.assertThat(files.get("AccountOneOfWrapper.java")) + JavaFileAssert.assertThat(files.get("AccountOneOf.java")) .isNormalClass() .assertTypeAnnotations().doesNotContainWithName("JsonSubTypes").toType() - .fileDoesNotContain("AccountOneOfWrapperMixin", "@JsonCreator"); + .fileDoesNotContain("Mixin", "@JsonCreator"); } } From e04af458933f939f493d67ac0905e2a29059c1cd Mon Sep 17 00:00:00 2001 From: jpfinne Date: Thu, 14 May 2026 20:06:09 +0200 Subject: [PATCH 12/18] Normalize mixed content --- .../main/java/org/openapitools/codegen/OpenAPINormalizer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 679e0afc1eef..8c229b98afee 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -776,11 +776,11 @@ public Schema normalizeSchema(Schema schema, Set visitedSchemas) { } if (ModelUtils.hasOneOf(schema)) { - return normalizeOneOf(schema, visitedSchemas); + schema = normalizeOneOf(schema, visitedSchemas); } if (ModelUtils.hasAnyOf(schema)) { - return normalizeAnyOf(schema, visitedSchemas); + schema = normalizeAnyOf(schema, visitedSchemas); } if (ModelUtils.hasProperties(schema)) { From 997b859527c1e8bc75b576e73c0c1cd43a21303d Mon Sep 17 00:00:00 2001 From: jpfinne Date: Thu, 14 May 2026 22:04:43 +0200 Subject: [PATCH 13/18] Rename USE_UNWRAPPED_FOR_COMPOSITE_ONEOF --- docs/customization.md | 24 +++++-------------- .../codegen/OpenAPINormalizer.java | 6 ++--- .../codegen/java/JavaClientCodegenTest.java | 2 +- .../java/spring/SpringCodegenTest.java | 13 ++++++---- .../resources/3_0/oneOf_unwrap_mixed.yaml | 20 +++++++++------- 5 files changed, 31 insertions(+), 34 deletions(-) diff --git a/docs/customization.md b/docs/customization.md index d446d63188f4..cc731bacfb65 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -681,11 +681,11 @@ index 6f27abd..146c61c 100644 type: string ``` -- `USE_UNWRAPPED_FOR_INLINE_ONEOF` set to true to unwrap inline oneOf combined with allOf/properties and without discriminator (aka mixed oneOf). Set the vendorExtension X_ONE_OF_UNWRAPPED to be used by generators. For example the java generator annotates the new oneOf property with the jackson `@JsonUnwrapped` annotation +- `USE_UNWRAPPED_FOR_COMPOSITE_ONEOF` set to true to unwrap oneOf combined with allOf/properties and without discriminator (aka "composite" oneOf). Set the vendorExtension X_ONE_OF_UNWRAPPED to be used by generators. For example the java generator annotates the new oneOf property with the jackson `@JsonUnwrapped` annotation Example: ``` -java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java -i modules/openapi-generator/src/test/resources/3_0/oneOf_unwrap_mixed.yaml -o /tmp/java/ --openapi-normalizer X_ONE_OF_UNWRAPPED=true +java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java -i modules/openapi-generator/src/test/resources/3_0/oneOf_unwrap_mixed.yaml -o /tmp/java/ --openapi-normalizer USE_UNWRAPPED_FOR_COMPOSITE_ONEOF=true ``` Here is what the change in the spec looks like: @@ -698,28 +698,16 @@ index 6f27abd..146c61c 100644 schemas: Account: - oneOf: -- - properties: -- bankNumber: -- type: string -- bic: -- type: string -- - properties: -- iban: -- type: string +- - $ref: '#/components/schemas/LegacyBankNumber' +- - ref: '#/components/schemas/WireTransferInfo' properties: bank: $ref: '#/components/schemas/Bank' + oneOf: + X_ONE_OF_UNWRAPPED: true + oneOf: -+ - properties: -+ bankNumber: -+ type: string -+ bic: -+ type: string -+ - properties: -+ iban: -+ type: string ++ - ref: '#/components/schemas/LegacyBankNumber' ++ - ref: '#/components/schemas/WireTransferInfo' ``` - `FILTER` diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 8c229b98afee..b7ca9ed3330b 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -159,7 +159,7 @@ public class OpenAPINormalizer { // when set to true, sort model properties by name to ensure deterministic output final String SORT_MODEL_PROPERTIES = "SORT_MODEL_PROPERTIES"; - final String USE_UNWRAPPED_FOR_INLINE_ONEOF = "USE_UNWRAPPED_FOR_INLINE_ONEOF"; + final String USE_UNWRAPPED_FOR_COMPOSITE_ONEOF = "USE_UNWRAPPED_FOR_COMPOSITE_ONEOF"; // ============= end of rules ============= /** @@ -220,7 +220,7 @@ public OpenAPINormalizer(OpenAPI openAPI, Map inputRules) { ruleNames.add(REMOVE_PROPERTIES_FROM_TYPE_OTHER_THAN_OBJECT); ruleNames.add(SORT_MODEL_PROPERTIES); ruleNames.add(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING); - ruleNames.add(USE_UNWRAPPED_FOR_INLINE_ONEOF); + ruleNames.add(USE_UNWRAPPED_FOR_COMPOSITE_ONEOF); // rules that are default to true rules.put(SIMPLIFY_ONEOF_ANYOF, true); @@ -1092,7 +1092,7 @@ protected Schema normalizeOneOf(Schema schema, Set visitedSchemas) { } protected Schema processUnwrappedOneOf(Schema schema) { - if (!getRule(USE_UNWRAPPED_FOR_INLINE_ONEOF)) { + if (!getRule(USE_UNWRAPPED_FOR_COMPOSITE_ONEOF)) { return schema; } if (!(ModelUtils.hasOneOf(schema) && (ModelUtils.hasProperties(schema) || ModelUtils.hasAllOf(schema)))) { diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java index c0ccbe22bb89..674d118bef3a 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java @@ -4457,7 +4457,7 @@ void unwrapped_oneOf_with_inheritance_sb3() throws IOException { final Map files = generateFromContract("src/test/resources/3_0/oneOf_unwrap_mixed.yaml", RESTCLIENT, Map.of(AbstractJavaCodegen.USE_ONE_OF_INTERFACES, true, USE_SPRING_BOOT4, false), - configurator -> configurator.setOpenapiNormalizer(Map.of("USE_UNWRAPPED_FOR_INLINE_ONEOF", "true"))); + configurator -> configurator.setOpenapiNormalizer(Map.of("USE_UNWRAPPED_FOR_COMPOSITE_ONEOF", "true"))); JavaFileAssert.assertThat(files.get("Account.java")) .assertProperty("oneOf") 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 e04b4b6a6deb..ef746076b2ce 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 @@ -7733,7 +7733,7 @@ void unwrapped_oneOf_with_inheritance_sb4() throws IOException { USE_DEDUCTION_FOR_ONE_OF_INTERFACES, true, USE_SPRING_BOOT4, true, USE_JACKSON_3, true), - configurator -> configurator.setOpenapiNormalizer(Map.of("USE_UNWRAPPED_FOR_INLINE_ONEOF", "true"))); + configurator -> configurator.setOpenapiNormalizer(Map.of("USE_UNWRAPPED_FOR_COMPOSITE_ONEOF", "true"))); JavaFileAssert.assertThat(files.get("Account.java")) .assertProperty("oneOf") @@ -7743,7 +7743,8 @@ void unwrapped_oneOf_with_inheritance_sb4() throws IOException { JavaFileAssert.assertThat(files.get("AccountOneOf.java")) .isInterface() .assertTypeAnnotations().doesNotContainWithName("JsonSubTypes").toType() - .fileContains("static interface AccountOneOfMixin", "@JsonCreator") + .fileContains("static interface AccountOneOfMixin", "@JsonCreator", + "@JsonSubTypes.Type(value = LegacyBankNumber.class)", "@JsonSubTypes.Type(value = WireTransferInfo.class)") .hasImports("tools.jackson.databind.JsonNode"); JavaFileAssert.assertThat(files.get("JacksonMixinConfig.java")) .fileContains(".addMixIn(AccountOneOf.class, AccountOneOf.AccountOneOfMixin.class)", @@ -7762,7 +7763,7 @@ void unwrapped_oneOf_with_inheritance_sb3() throws IOException { USE_DEDUCTION_FOR_ONE_OF_INTERFACES, true, USE_SPRING_BOOT3, true) , - configurator -> configurator.setOpenapiNormalizer(Map.of("USE_UNWRAPPED_FOR_INLINE_ONEOF", "true"))); + configurator -> configurator.setOpenapiNormalizer(Map.of("USE_UNWRAPPED_FOR_COMPOSITE_ONEOF", "true"))); JavaFileAssert.assertThat(files.get("Account.java")) .assertProperty("oneOf") @@ -7790,14 +7791,18 @@ void unwrapped_oneOf_with_composition() throws IOException { USE_DEDUCTION_FOR_ONE_OF_INTERFACES, false, USE_SPRING_BOOT4, true, USE_JACKSON_3, true), - configurator -> configurator.setOpenapiNormalizer(Map.of("USE_UNWRAPPED_FOR_INLINE_ONEOF", "true"))); + configurator -> configurator.setOpenapiNormalizer(Map.of("USE_UNWRAPPED_FOR_COMPOSITE_ONEOF", "true"))); JavaFileAssert.assertThat(files.get("Account.java")) .assertProperty("oneOf") .doesImportAnnotation("JsonUnwrapped") .assertPropertyAnnotations().containsWithName("JsonUnwrapped"); JavaFileAssert.assertThat(files.get("AccountOneOf.java")) .isNormalClass() + .assertProperty("bankNumber").toType() + .assertProperty("iban").toType() + .assertProperty("bic").toType() .assertTypeAnnotations().doesNotContainWithName("JsonSubTypes").toType() .fileDoesNotContain("Mixin", "@JsonCreator"); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/oneOf_unwrap_mixed.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_unwrap_mixed.yaml index 0dea9e220012..2edfa98e330e 100644 --- a/modules/openapi-generator/src/test/resources/3_0/oneOf_unwrap_mixed.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/oneOf_unwrap_mixed.yaml @@ -25,14 +25,18 @@ components: bank: $ref: '#/components/schemas/Bank' oneOf: - - properties: - bankNumber: - type: string - bic: - type: string - - properties: - iban: - type: string + - $ref: '#/components/schemas/LegacyBankNumber' + - $ref: '#/components/schemas/WireTransferInfo' + LegacyBankNumber: + properties: + bankNumber: + type: string + WireTransferInfo: + properties: + iban: + type: string + bic: + type: string Bank: allOf: - properties: From 5f0bfe1ce74b92c115b3bd9c551de40bc3c72fd7 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Thu, 14 May 2026 22:12:05 +0200 Subject: [PATCH 14/18] Rename method to processUnwrapCompositeOneOf --- .../main/java/org/openapitools/codegen/OpenAPINormalizer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index b7ca9ed3330b..ef2de4a8a032 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -1081,7 +1081,7 @@ protected Schema normalizeOneOf(Schema schema, Set visitedSchemas) { schema.getOneOf().set(i, normalizeSchema((Schema) item, visitedSchemas)); } schema = processReplaceOneOfByMapping(schema); - schema = processUnwrappedOneOf(schema); + schema = processUnwrapCompositeOneOf(schema); } else { // normalize it as it's no longer an oneOf @@ -1091,7 +1091,7 @@ protected Schema normalizeOneOf(Schema schema, Set visitedSchemas) { return schema; } - protected Schema processUnwrappedOneOf(Schema schema) { + protected Schema processUnwrapCompositeOneOf(Schema schema) { if (!getRule(USE_UNWRAPPED_FOR_COMPOSITE_ONEOF)) { return schema; } From 5ebc7e2ce9847e42ba6ca7b3c539307e20b48e59 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Thu, 14 May 2026 22:55:30 +0200 Subject: [PATCH 15/18] Move more constants to CodegenConstants --- .../java/org/openapitools/codegen/CodegenConstants.java | 6 +++++- .../codegen/languages/AbstractJavaCodegen.java | 2 +- .../codegen/languages/AbstractKotlinCodegen.java | 4 +--- .../openapitools/codegen/languages/JavaClientCodegen.java | 6 ------ .../org/openapitools/codegen/languages/SpringCodegen.java | 7 +------ 5 files changed, 8 insertions(+), 17 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java index 2a8184f5e3aa..dce069fe72dc 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java @@ -501,5 +501,9 @@ public static enum ENUM_PROPERTY_NAMING_TYPE {camelCase, PascalCase, snake_case, public static final String X_NULLABLE = "x-nullable"; public static final String X_ONE_OF_JSON_CREATOR = "x-oneof-jsonCreator"; - public static final String X_ONE_OF_UNWRAPPED = "x-oneOfunwrapper"; + public static final String X_ONE_OF_UNWRAPPED = "x-oneOfunwrapped"; + public static final String JACKSON2_PACKAGE = "com.fasterxml.jackson"; + public static final String JACKSON3_PACKAGE = "tools.jackson"; + public static final String JACKSON_PACKAGE = "jacksonPackage"; + } 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 3cd9e86fe187..65c7eae86d66 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 @@ -2866,7 +2866,7 @@ protected void addOneOfMixinSupport(ModelsMap obj, CodegenModel cm) { if (!isUseJackson3()) { obj.getImports().add(Map.of("import", "com.fasterxml.jackson.core.JsonProcessingException")); } - obj.getImports().add(Map.of("import", importMapping.get("JsonNode"))); + obj.getImports().add(Map.of("import", (isUseJackson3()? JACKSON3_PACKAGE:JACKSON2_PACKAGE) + ".databind.JsonNode")); obj.getImports().add(Map.of("import", configPackage + ".JacksonMixinConfig")); } 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 d3b8e0f16bbc..d50ee8fd587b 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 @@ -46,6 +46,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.openapitools.codegen.CodegenConstants.*; import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER; import static org.openapitools.codegen.utils.StringUtils.*; @@ -58,9 +59,6 @@ public abstract class AbstractKotlinCodegen extends DefaultCodegen implements Co public static final String JAVAX_PACKAGE = "javaxPackage"; public static final String USE_JAKARTA_EE = "useJakartaEe"; public static final String USE_JACKSON_3 = "useJackson3"; - public static final String JACKSON2_PACKAGE = "com.fasterxml.jackson"; - public static final String JACKSON3_PACKAGE = "tools.jackson"; - public static final String JACKSON_PACKAGE = "jacksonPackage"; public static final String SCHEMA_IMPLEMENTS = "schemaImplements"; public static final String SCHEMA_IMPLEMENTS_FIELDS = "schemaImplementsFields"; public static final String X_KOTLIN_IMPLEMENTS_SKIP = "xKotlinImplementsSkip"; diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java index db47af759e40..8b26a63ecb63 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java @@ -127,10 +127,6 @@ public class JavaClientCodegen extends AbstractJavaCodegen public static final String SERIALIZATION_LIBRARY_JSONB = "jsonb"; public static final String USE_SPRING_BOOT4 = "useSpringBoot4"; - private static final String JACKSON2_PACKAGE = "com.fasterxml.jackson"; - private static final String JACKSON3_PACKAGE = "tools.jackson"; - private static final String JACKSON_PACKAGE = "jacksonPackage"; - public static final String GENERATE_CLIENT_AS_BEAN = "generateClientAsBean"; protected String gradleWrapperPackage = "gradle.wrapper"; @@ -1322,13 +1318,11 @@ public void setCaseInsensitiveResponseHeaders(final Boolean caseInsensitiveRespo protected void applyJackson2Package() { writePropertyBack(JACKSON_PACKAGE, JACKSON2_PACKAGE); - importMapping.put("JsonNode", JACKSON2_PACKAGE + ".databind.JsonNode"); importMapping.put("JsonMapper", JACKSON2_PACKAGE + ".databind.json.JsonMapper"); } protected void applyJackson3Package() { writePropertyBack(JACKSON_PACKAGE, JACKSON3_PACKAGE); - importMapping.put("JsonNode", JACKSON3_PACKAGE + ".databind.JsonNode"); importMapping.put("JsonMapper", JACKSON3_PACKAGE + ".databind.json.JsonMapper"); } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java index 317d35fcf680..744c930bff5f 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java @@ -54,8 +54,7 @@ import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isNotEmpty; -import static org.openapitools.codegen.CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES; -import static org.openapitools.codegen.CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES_DESC; +import static org.openapitools.codegen.CodegenConstants.*; import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER; import static org.openapitools.codegen.utils.StringUtils.camelize; @@ -114,9 +113,6 @@ public class SpringCodegen extends AbstractJavaCodegen public static final String USE_SPRING_BUILT_IN_VALIDATION = "useSpringBuiltInValidation"; public static final String SPRING_API_VERSION = "springApiVersion"; public static final String USE_JACKSON_3 = "useJackson3"; - public static final String JACKSON2_PACKAGE = "com.fasterxml.jackson"; - public static final String JACKSON3_PACKAGE = "tools.jackson"; - public static final String JACKSON_PACKAGE = "jacksonPackage"; public static final String ADDITIONAL_NOT_NULL_ANNOTATIONS = "additionalNotNullAnnotations"; public static final String AUTO_X_SPRING_PAGINATED = "autoXSpringPaginated"; public static final String GENERATE_SORT_VALIDATION = "generateSortValidation"; @@ -607,7 +603,6 @@ public void processOpts() { importMapping.put("DateTimeFormat", "org.springframework.format.annotation.DateTimeFormat"); importMapping.put("ParameterObject", "org.springdoc.api.annotations.ParameterObject"); String jacksonPackage = (String)additionalProperties.get("jacksonPackage"); - importMapping.put("JsonNode", jacksonPackage + ".databind.JsonNode"); importMapping.put("JsonMapper", jacksonPackage + ".databind.json.JsonMapper"); if (isUseSpringBoot3() || isUseSpringBoot4()) { From 6f046f003fb62de1be95cf25b99d0f63a8b29f3a Mon Sep 17 00:00:00 2001 From: jpfinne Date: Thu, 14 May 2026 21:59:41 -0300 Subject: [PATCH 16/18] Update modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- .../main/java/org/openapitools/codegen/OpenAPINormalizer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index ef2de4a8a032..4a96a1a79694 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -1105,7 +1105,7 @@ protected Schema processUnwrapCompositeOneOf(Schema schema) { boolean hasDiscriminator = schema.getDiscriminator() != null; if (hasDiscriminator) { List oneOfs = schema.getOneOf(); - if (oneOfs.stream().allMatch(oneOf -> oneOf.get$ref() != null)) { + if (oneOfs.stream().allMatch(oneOf -> oneOf != null && oneOf.get$ref() != null)) { // skip normalization if discriminator but not maping if (discriminator.getMapping() == null && discriminator.getPropertyName() != null) { return schema; From ed075ceea93c3c3b2d28e45c4e0b229155879e86 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Fri, 15 May 2026 20:26:52 +0200 Subject: [PATCH 17/18] use function skipUnwrapOneOf --- .../codegen/OpenAPINormalizer.java | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 4a96a1a79694..b3df36b9920c 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -1095,28 +1095,10 @@ protected Schema processUnwrapCompositeOneOf(Schema schema) { if (!getRule(USE_UNWRAPPED_FOR_COMPOSITE_ONEOF)) { return schema; } - if (!(ModelUtils.hasOneOf(schema) && (ModelUtils.hasProperties(schema) || ModelUtils.hasAllOf(schema)))) { + if (skipUnwrapOneOf(schema)) { return schema; } - // skip handling of oneOf + properties + discriminator - // accept discriminator mappings NOT matching the oneOf elements. - Discriminator discriminator = schema.getDiscriminator(); - boolean hasDiscriminator = schema.getDiscriminator() != null; - if (hasDiscriminator) { - List oneOfs = schema.getOneOf(); - if (oneOfs.stream().allMatch(oneOf -> oneOf != null && oneOf.get$ref() != null)) { - // skip normalization if discriminator but not maping - if (discriminator.getMapping() == null && discriminator.getPropertyName() != null) { - return schema; - } - // if same size for mapping and oneOf, assume that we skip this normalization - if (discriminator.getMapping() != null && discriminator.getMapping().size() == schema.getOneOf().size()) { - return schema; - } - } - } - Schema newOneOfSchema = new ComposedSchema(); newOneOfSchema.oneOf(new ArrayList<>(schema.getOneOf())); newOneOfSchema.addExtension(X_ONE_OF_UNWRAPPED, true); @@ -1134,6 +1116,36 @@ protected Schema processUnwrapCompositeOneOf(Schema schema) { return schema; } + /** + * Check if unwrapping oneOf is not relevant. + * + * @return true if unwrapping of OneOf must be skipped + */ + protected boolean skipUnwrapOneOf(Schema schema) { + if (!(ModelUtils.hasOneOf(schema) && (ModelUtils.hasProperties(schema) || ModelUtils.hasAllOf(schema)))) { + return true; + } + + // skip handling of oneOf + properties + discriminator + // accept discriminator mappings NOT matching the oneOf elements. + Discriminator discriminator = schema.getDiscriminator(); + boolean hasDiscriminator = schema.getDiscriminator() != null; + if (hasDiscriminator) { + List oneOfs = schema.getOneOf(); + if (oneOfs.stream().allMatch(oneOf -> oneOf != null && oneOf.get$ref() != null)) { + // skip normalization if discriminator but not maping + if (discriminator.getMapping() == null && discriminator.getPropertyName() != null) { + return true; + } + // if same size for mapping and oneOf, assume that we skip this normalization + if (discriminator.getMapping() != null && discriminator.getMapping().size() == schema.getOneOf().size()) { + return true; + } + } + } + return false; + } + protected Schema normalizeAnyOf(Schema schema, Set visitedSchemas) { //transform anyOf into enums if needed schema = processSimplifyAnyOfEnum(schema); From f032a06a0470ba25c3eb7b5a58941c798b396709 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Sat, 16 May 2026 12:47:19 +0200 Subject: [PATCH 18/18] Force rebuild --- .../main/java/org/openapitools/codegen/OpenAPINormalizer.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index fc5595d76b5c..aa12f59aae09 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -1091,6 +1091,9 @@ protected Schema normalizeOneOf(Schema schema, Set visitedSchemas) { return schema; } + /* + * handling of USE_UNWRAPPED_FOR_COMPOSITE_ONEOF rule. + */ protected Schema processUnwrapCompositeOneOf(Schema schema) { if (!getRule(USE_UNWRAPPED_FOR_COMPOSITE_ONEOF)) { return schema;