From c22e0918f21ac6c6b2906c1911c058ee0347179d Mon Sep 17 00:00:00 2001 From: Kanna Kim Date: Sun, 30 Aug 2026 21:36:22 -0500 Subject: [PATCH 1/4] fix(java): make nested enums serializable with serializableModel Recursively traverse model properties and add Serializable to inline enums. Add regression coverage for nested schema locations and recursive graphs. Fixes #23082 --- .../languages/AbstractJavaCodegen.java | 71 ++++++++++- .../codegen/java/JavaClientCodegenTest.java | 110 ++++++++++++++++++ .../src/test/resources/3_0/issue_23082.yaml | 27 +++++ .../openapitools/client/model/SomeObj.java | 2 +- 4 files changed, 204 insertions(+), 6 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/issue_23082.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java index 554f98183fc7..12d5e45f5334 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 @@ -67,6 +67,7 @@ import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.ConcurrentSkipListSet; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -2214,11 +2215,12 @@ public ModelsMap postProcessModels(ModelsMap objs) { if (this.serializableModel) { for (ModelMap mo : objs.getModels()) { CodegenModel cm = mo.getModel(); - List xImplements = new ArrayList<>(getObjectAsStringList(cm.getVendorExtensions().get(X_IMPLEMENTS))); - if (!xImplements.contains("Serializable")) { - xImplements.add("Serializable"); - } - cm.getVendorExtensions().replace(X_IMPLEMENTS, xImplements); + addInterfaceToVendorExtensions(cm.getVendorExtensions(), "Serializable"); + forEachModelPropertyRecursively(cm, property -> { + if (property.isEnum) { + addInterfaceToVendorExtensions(property.getVendorExtensions(), "Serializable"); + } + }); } } @@ -2303,6 +2305,65 @@ private void normalizeModelPropertyVendorExtensions(CodegenModel model, String n } } + private void addInterfaceToVendorExtensions(Map vendorExtensions, String interfaceName) { + List interfaces = new ArrayList<>(getObjectAsStringList(vendorExtensions.get(X_IMPLEMENTS))); + if (!interfaces.contains(interfaceName)) { + interfaces.add(interfaceName); + } + vendorExtensions.put(X_IMPLEMENTS, interfaces); + } + + private void forEachModelPropertyRecursively(CodegenModel model, Consumer action) { + Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); + visitNestedProperties(model, visited, action); + } + + private void visitProperty(CodegenProperty property, Set visited, + Consumer action) { + if (property == null || !visited.add(property)) { + return; + } + + action.accept(property); + visitNestedProperties(property, visited, action); + } + + /** + * Visits every canonical location in which a schema can contain another {@link CodegenProperty}. + * Derived views such as {@code requiredVars} and {@code mostInnerItems} are intentionally omitted + * because their properties are already reachable through {@code vars} and {@code items}. + */ + private void visitNestedProperties(IJsonSchemaValidationProperties schema, + Set visited, Consumer action) { + visitProperties(schema.getVars(), visited, action); + visitProperty(schema.getItems(), visited, action); + visitProperty(schema.getAdditionalProperties(), visited, action); + visitProperty(schema.getContains(), visited, action); + + Map requiredVarsMap = schema.getRequiredVarsMap(); + if (requiredVarsMap != null) { + visitProperties(requiredVarsMap.values(), visited, action); + } + + CodegenComposedSchemas composedSchemas = schema.getComposedSchemas(); + if (composedSchemas != null) { + visitProperties(composedSchemas.getAllOf(), visited, action); + visitProperties(composedSchemas.getOneOf(), visited, action); + visitProperties(composedSchemas.getAnyOf(), visited, action); + visitProperty(composedSchemas.getNot(), visited, action); + } + } + + private void visitProperties(Collection properties, Set visited, + Consumer action) { + if (properties == null) { + return; + } + for (CodegenProperty property : properties) { + visitProperty(property, visited, action); + } + } + /** * Normalizes an operation parameter vendor extension across all parameter collections. * In this context, normalization means converting a missing value, a single string, or a list value 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 5d5b31c25b51..b5e286aff292 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 @@ -43,6 +43,8 @@ import org.openapitools.codegen.languages.features.BeanValidationFeatures; import org.openapitools.codegen.languages.features.CXFServerFeatures; import org.openapitools.codegen.meta.features.SecurityFeature; +import org.openapitools.codegen.model.ModelMap; +import org.openapitools.codegen.model.ModelsMap; import org.openapitools.codegen.model.OperationMap; import org.openapitools.codegen.model.OperationsMap; import org.openapitools.codegen.testutils.ConfigAssert; @@ -4088,6 +4090,114 @@ public void testEnumWithImplements() { JavaFileAssert.assertThat(files.get("Type.java")).fileContains("Type implements java.io.Serializable {"); } + @Test + public void testSerializableModelAddsSerializableToInnerEnums_issue23082() { + Map files = generateFromContract( + "src/test/resources/3_0/issue_23082.yaml", + JavaClientCodegen.OKHTTP_GSON, + Map.of(SERIALIZABLE_MODEL, true)); + + JavaFileAssert.assertThat(files.get("ExampleWithInnerEnums.java")).fileContains( + "class ExampleWithInnerEnums implements Serializable {", + "enum ExampleInnerEnum implements Serializable {", + "enum NestedExamplesEnum implements Serializable {"); + } + + @Test + public void testSerializableModelTraversesEveryNestedPropertyLocation_issue23082() { + JavaClientCodegen codegen = new JavaClientCodegen(); + codegen.additionalProperties().put(SERIALIZABLE_MODEL, true); + codegen.processOpts(); + + CodegenModel model = new CodegenModel(); + CodegenProperty container = new CodegenProperty(); + // components.schemas.Model.properties.container + model.vars.add(container); + + CodegenProperty directEnum = newEnumProperty(); + // properties.directEnum + container.vars.add(directEnum); + + CodegenProperty arrayEnum = newEnumProperty(); + CodegenProperty nestedArray = new CodegenProperty(); + // type: array + // items: + // type: array + // items: { type: string, enum: [...] } + nestedArray.setItems(arrayEnum); + container.setItems(nestedArray); + + CodegenProperty additionalPropertiesEnum = newEnumProperty(); + // additionalProperties: { type: string, enum: [...] } + container.setAdditionalProperties(additionalPropertiesEnum); + + CodegenProperty containsEnum = newEnumProperty(); + // contains: { type: string, enum: [...] } (OpenAPI 3.1) + container.setContains(containsEnum); + + CodegenProperty requiredPropertyEnum = newEnumProperty(); + // required: [requiredProperty] + // properties.requiredProperty: { type: string, enum: [...] } + container.setRequiredVarsMap(Map.of("requiredProperty", requiredPropertyEnum)); + + CodegenProperty allOfEnum = newEnumProperty(); + CodegenProperty oneOfEnum = newEnumProperty(); + CodegenProperty anyOfEnum = newEnumProperty(); + CodegenProperty notEnum = newEnumProperty(); + // allOf: [{ type: string, enum: [...] }] + // oneOf: [{ type: string, enum: [...] }] + // anyOf: [{ type: string, enum: [...] }] + // not: { type: string, enum: [...] } + container.setComposedSchemas(new CodegenComposedSchemas( + List.of(allOfEnum), List.of(oneOfEnum), List.of(anyOfEnum), notEnum)); + + // Exercise model-level schema locations as well as property-level locations. + CodegenProperty modelItemsEnum = newEnumProperty(); + // components.schemas.Model: { type: array, items: { type: string, enum: [...] } } + model.setItems(modelItemsEnum); + CodegenProperty modelAdditionalPropertiesEnum = newEnumProperty(); + // components.schemas.Model: { type: object, additionalProperties: { type: string, enum: [...] } } + model.setAdditionalProperties(modelAdditionalPropertiesEnum); + CodegenProperty modelContainsEnum = newEnumProperty(); + // components.schemas.Model: { type: array, contains: { type: string, enum: [...] } } + model.setContains(modelContainsEnum); + + // A malformed or mutually recursive graph must not cause unbounded traversal. + container.vars.add(container); + + ModelMap modelMap = new ModelMap(); + modelMap.setModel(model); + ModelsMap modelsMap = new ModelsMap(); + modelsMap.setModels(List.of(modelMap)); + modelsMap.setImports(new ArrayList<>()); + + codegen.postProcessModels(modelsMap); + + List nestedEnums = List.of( + directEnum, + arrayEnum, + additionalPropertiesEnum, + containsEnum, + requiredPropertyEnum, + allOfEnum, + oneOfEnum, + anyOfEnum, + notEnum, + modelItemsEnum, + modelAdditionalPropertiesEnum, + modelContainsEnum); + for (CodegenProperty nestedEnum : nestedEnums) { + assertThat(nestedEnum.getVendorExtensions()) + .containsEntry(X_IMPLEMENTS, List.of("Serializable")); + } + } + + private static CodegenProperty newEnumProperty() { + CodegenProperty property = new CodegenProperty(); + property.isEnum = true; + return property; + } + /** * This checks bug issue-20718 * A situation when schemaMapping is used and oneOf also is used with one of the schema-mapped dataTypes and the dataType diff --git a/modules/openapi-generator/src/test/resources/3_0/issue_23082.yaml b/modules/openapi-generator/src/test/resources/3_0/issue_23082.yaml new file mode 100644 index 000000000000..9b404da7561a --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/issue_23082.yaml @@ -0,0 +1,27 @@ +openapi: 3.0.3 +info: + title: Issue 23082 reproduction + version: 1.0.0 +paths: {} +components: + schemas: + ExampleWithInnerEnums: + type: object + description: Example with inner enum class + properties: + exampleInner: + type: string + description: Example of an inner enum class + enum: + - ENUM1 + - ENUM2 + - ENUM3 + nestedExamples: + type: array + items: + type: array + items: + type: string + enum: + - NESTED1 + - NESTED2 diff --git a/samples/client/others/java/okhttp-gson-streaming/src/main/java/org/openapitools/client/model/SomeObj.java b/samples/client/others/java/okhttp-gson-streaming/src/main/java/org/openapitools/client/model/SomeObj.java index 5e3f6aad002a..afbbc359c1ab 100644 --- a/samples/client/others/java/okhttp-gson-streaming/src/main/java/org/openapitools/client/model/SomeObj.java +++ b/samples/client/others/java/okhttp-gson-streaming/src/main/java/org/openapitools/client/model/SomeObj.java @@ -58,7 +58,7 @@ public class SomeObj implements Serializable { * Gets or Sets $type */ @JsonAdapter(TypeEnum.Adapter.class) - public enum TypeEnum { + public enum TypeEnum implements Serializable { SOMEOBJIDENTIFIER("SomeObjIdentifier"); private String value; From ad018e1df5de624ab3d4deefb5242f90f0c8825e Mon Sep 17 00:00:00 2001 From: Ori <47150013+KannaKim@users.noreply.github.com> Date: Sun, 30 Aug 2026 22:35:07 -0500 Subject: [PATCH 2/4] Update modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- .../openapitools/codegen/languages/AbstractJavaCodegen.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 12d5e45f5334..0ac688337538 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 @@ -2330,7 +2330,9 @@ private void visitProperty(CodegenProperty property, Set visite /** * Visits every canonical location in which a schema can contain another {@link CodegenProperty}. - * Derived views such as {@code requiredVars} and {@code mostInnerItems} are intentionally omitted ++ * Other derived views such as {@code mostInnerItems} are intentionally omitted ++ * because their properties are already reachable through {@code items}. Note that ++ * {@code requiredVarsMap} is traversed explicitly below. * because their properties are already reachable through {@code vars} and {@code items}. */ private void visitNestedProperties(IJsonSchemaValidationProperties schema, From 73c3a911b83071ae4d046f41b3adf53909d35df2 Mon Sep 17 00:00:00 2001 From: Ori <47150013+KannaKim@users.noreply.github.com> Date: Sun, 30 Aug 2026 23:14:19 -0500 Subject: [PATCH 3/4] Update modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- .../codegen/languages/AbstractJavaCodegen.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 0ac688337538..3d756f725e00 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 @@ -2330,10 +2330,9 @@ private void visitProperty(CodegenProperty property, Set visite /** * Visits every canonical location in which a schema can contain another {@link CodegenProperty}. -+ * Other derived views such as {@code mostInnerItems} are intentionally omitted -+ * because their properties are already reachable through {@code items}. Note that -+ * {@code requiredVarsMap} is traversed explicitly below. - * because their properties are already reachable through {@code vars} and {@code items}. + * Other derived views such as {@code mostInnerItems} are intentionally omitted + * because their properties are already reachable through {@code items}. Note that + * {@code requiredVarsMap} is traversed explicitly below. */ private void visitNestedProperties(IJsonSchemaValidationProperties schema, Set visited, Consumer action) { From 4534776b71bab66da9c7c70d20101bb2bc76cc52 Mon Sep 17 00:00:00 2001 From: Kanna Kim Date: Wed, 2 Sep 2026 00:05:26 -0500 Subject: [PATCH 4/4] refactor: move recursive model property traversal to DefaultCodegen chore: add docstring to addInterfaceToVendorExtensions method in AbstractJavaCodegen.java --- .../openapitools/codegen/DefaultCodegen.java | 65 +++++++++++++++++++ .../languages/AbstractJavaCodegen.java | 60 ++--------------- 2 files changed, 72 insertions(+), 53 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 e8f7eb86e02a..64a1ba9b843c 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 @@ -4610,6 +4610,71 @@ protected CodegenProperty getMostInnerItems(CodegenProperty property) { return currentProperty; } + /** + * Applies an action to every property recursively reachable from a model. + * Properties are de-duplicated by identity to avoid repeated processing and cycles. + * + * @param model model whose properties should be visited + * @param action action to apply to each property + */ + protected void forEachModelPropertyRecursively(CodegenModel model, Consumer action) { + Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); + visitNestedProperties(model, visited, action); + } + + /** + * Applies an action to a property and recursively visits its nested properties. + */ + private void visitProperty(CodegenProperty property, Set visited, + Consumer action) { + if (property == null || !visited.add(property)) { + return; + } + + action.accept(property); + visitNestedProperties(property, visited, action); + } + + /** + * Visits every canonical location in which a schema can contain another {@link CodegenProperty}. + * Other derived views such as {@code mostInnerItems} are intentionally omitted + * because their properties are already reachable through {@code items}. Note that + * {@code requiredVarsMap} is traversed explicitly below. + */ + private void visitNestedProperties(IJsonSchemaValidationProperties schema, + Set visited, Consumer action) { + visitProperties(schema.getVars(), visited, action); + visitProperty(schema.getItems(), visited, action); + visitProperty(schema.getAdditionalProperties(), visited, action); + visitProperty(schema.getContains(), visited, action); + + Map requiredVarsMap = schema.getRequiredVarsMap(); + if (requiredVarsMap != null) { + visitProperties(requiredVarsMap.values(), visited, action); + } + + CodegenComposedSchemas composedSchemas = schema.getComposedSchemas(); + if (composedSchemas != null) { + visitProperties(composedSchemas.getAllOf(), visited, action); + visitProperties(composedSchemas.getOneOf(), visited, action); + visitProperties(composedSchemas.getAnyOf(), visited, action); + visitProperty(composedSchemas.getNot(), visited, action); + } + } + + /** + * Visits each property in a collection and its nested properties. + */ + private void visitProperties(Collection properties, Set visited, + Consumer action) { + if (properties == null) { + return; + } + for (CodegenProperty property : properties) { + visitProperty(property, visited, action); + } + } + protected Map getInnerEnumAllowableValues(CodegenProperty property) { CodegenProperty currentProperty = getMostInnerItems(property); 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 3d756f725e00..d2cb03bcf1e9 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 @@ -67,7 +67,6 @@ import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.ConcurrentSkipListSet; -import java.util.function.Consumer; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -2305,6 +2304,13 @@ private void normalizeModelPropertyVendorExtensions(CodegenModel model, String n } } + /** + * Adds an interface to the {@code x-implements} vendor extension if it is not already present. + * The existing extension value is normalized to a mutable list of strings before it is updated. + * + * @param vendorExtensions vendor extension map to update + * @param interfaceName interface name to add + */ private void addInterfaceToVendorExtensions(Map vendorExtensions, String interfaceName) { List interfaces = new ArrayList<>(getObjectAsStringList(vendorExtensions.get(X_IMPLEMENTS))); if (!interfaces.contains(interfaceName)) { @@ -2313,58 +2319,6 @@ private void addInterfaceToVendorExtensions(Map vendorExtensions vendorExtensions.put(X_IMPLEMENTS, interfaces); } - private void forEachModelPropertyRecursively(CodegenModel model, Consumer action) { - Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); - visitNestedProperties(model, visited, action); - } - - private void visitProperty(CodegenProperty property, Set visited, - Consumer action) { - if (property == null || !visited.add(property)) { - return; - } - - action.accept(property); - visitNestedProperties(property, visited, action); - } - - /** - * Visits every canonical location in which a schema can contain another {@link CodegenProperty}. - * Other derived views such as {@code mostInnerItems} are intentionally omitted - * because their properties are already reachable through {@code items}. Note that - * {@code requiredVarsMap} is traversed explicitly below. - */ - private void visitNestedProperties(IJsonSchemaValidationProperties schema, - Set visited, Consumer action) { - visitProperties(schema.getVars(), visited, action); - visitProperty(schema.getItems(), visited, action); - visitProperty(schema.getAdditionalProperties(), visited, action); - visitProperty(schema.getContains(), visited, action); - - Map requiredVarsMap = schema.getRequiredVarsMap(); - if (requiredVarsMap != null) { - visitProperties(requiredVarsMap.values(), visited, action); - } - - CodegenComposedSchemas composedSchemas = schema.getComposedSchemas(); - if (composedSchemas != null) { - visitProperties(composedSchemas.getAllOf(), visited, action); - visitProperties(composedSchemas.getOneOf(), visited, action); - visitProperties(composedSchemas.getAnyOf(), visited, action); - visitProperty(composedSchemas.getNot(), visited, action); - } - } - - private void visitProperties(Collection properties, Set visited, - Consumer action) { - if (properties == null) { - return; - } - for (CodegenProperty property : properties) { - visitProperty(property, visited, action); - } - } - /** * Normalizes an operation parameter vendor extension across all parameter collections. * In this context, normalization means converting a missing value, a single string, or a list value