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 554f98183fc7..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 @@ -2214,11 +2214,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 +2304,21 @@ 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)) { + interfaces.add(interfaceName); + } + vendorExtensions.put(X_IMPLEMENTS, interfaces); + } + /** * 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;