Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<CodegenProperty> action) {
Set<CodegenProperty> 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<CodegenProperty> visited,
Consumer<CodegenProperty> 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<CodegenProperty> visited, Consumer<CodegenProperty> action) {
visitProperties(schema.getVars(), visited, action);
visitProperty(schema.getItems(), visited, action);
visitProperty(schema.getAdditionalProperties(), visited, action);
visitProperty(schema.getContains(), visited, action);

Map<String, CodegenProperty> 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<CodegenProperty> properties, Set<CodegenProperty> visited,
Consumer<CodegenProperty> action) {
if (properties == null) {
return;
}
for (CodegenProperty property : properties) {
visitProperty(property, visited, action);
}
}

protected Map<String, Object> getInnerEnumAllowableValues(CodegenProperty property) {
CodegenProperty currentProperty = getMostInnerItems(property);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2214,11 +2214,12 @@ public ModelsMap postProcessModels(ModelsMap objs) {
if (this.serializableModel) {
for (ModelMap mo : objs.getModels()) {
CodegenModel cm = mo.getModel();
List<String> 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");
}
});
}
}

Expand Down Expand Up @@ -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<String, Object> vendorExtensions, String interfaceName) {
Comment thread
KannaKim marked this conversation as resolved.
List<String> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, File> 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<CodegenProperty> 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
Expand Down
27 changes: 27 additions & 0 deletions modules/openapi-generator/src/test/resources/3_0/issue_23082.yaml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading