Skip to content

Commit 2c1ca02

Browse files
[codegen][Go] Fix compilation error of generated go code when schema is free form object (#5391)
* Fix code generation for free-form objects in go-experimental * Execute scripts in bin directory * Add more use cases for open-ended types * Add more use cases for open-ended types * Add more use cases for open-ended types * add code comments * Better name for test properties * handle scenario when type is arbitrary * handle interface{} scenario * handle interface{} scenario * add helper function isAnyType * isAnyType function * implementation of isAnyType function * fix javadoc issue * handle interface{} scenario * use equals comparison instead of == * merge from master * Add code documentation * add code comments, remove unused min/max attribute, fix equals method * Handle 'anytype' use case * add code comments * override postProcessModelProperty to set vendor extension * Use vendorExtensions.x-golang-is-container * fix compilation error of generated code * fix compilation error of generated code * fix compilation error of generated code
1 parent 256a431 commit 2c1ca02

18 files changed

Lines changed: 564 additions & 61 deletions

File tree

docs/generators/go-experimental.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ sidebar_label: go-experimental
4040
<li>int</li>
4141
<li>int32</li>
4242
<li>int64</li>
43+
<li>interface{}</li>
44+
<li>map[string]interface{}</li>
4345
<li>rune</li>
4446
<li>string</li>
4547
<li>uint</li>

docs/generators/go-gin-server.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ sidebar_label: go-gin-server
3333
<li>int</li>
3434
<li>int32</li>
3535
<li>int64</li>
36+
<li>interface{}</li>
37+
<li>map[string]interface{}</li>
3638
<li>rune</li>
3739
<li>string</li>
3840
<li>uint</li>

docs/generators/go-server.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ sidebar_label: go-server
3636
<li>int</li>
3737
<li>int32</li>
3838
<li>int64</li>
39+
<li>interface{}</li>
40+
<li>map[string]interface{}</li>
3941
<li>rune</li>
4042
<li>string</li>
4143
<li>uint</li>

docs/generators/go.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ sidebar_label: go
4040
<li>int</li>
4141
<li>int32</li>
4242
<li>int64</li>
43+
<li>interface{}</li>
44+
<li>map[string]interface{}</li>
4345
<li>rune</li>
4446
<li>string</li>
4547
<li>uint</li>

modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenProperty.java

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,43 @@
2020
import java.util.*;
2121

2222
public class CodegenProperty implements Cloneable, IJsonSchemaValidationProperties {
23-
public String openApiType, baseName, complexType, getter, setter, description, dataType,
24-
datatypeWithEnum, dataFormat, name, min, max, defaultValue, defaultValueWithParam,
25-
baseType, containerType, title;
23+
/**
24+
* The value of the 'type' attribute in the OpenAPI schema.
25+
* The per-language codegen logic may change to a language-specific type.
26+
*/
27+
public String openApiType;
28+
public String baseName;
29+
public String complexType;
30+
public String getter;
31+
public String setter;
32+
/**
33+
* The value of the 'description' attribute in the OpenAPI schema.
34+
*/
35+
public String description;
36+
/**
37+
* The language-specific data type for this property. For example, the OpenAPI type 'integer'
38+
* may be represented as 'int', 'int32', 'Integer', etc, depending on the programming language.
39+
*/
40+
public String dataType;
41+
public String datatypeWithEnum;
42+
public String dataFormat;
43+
/**
44+
* The name of this property in the OpenAPI schema.
45+
*/
46+
public String name;
47+
public String min; // TODO: is this really used?
48+
public String max; // TODO: is this really used?
49+
public String defaultValue;
50+
public String defaultValueWithParam;
51+
public String baseType;
52+
public String containerType;
53+
/**
54+
* The value of the 'title' attribute in the OpenAPI schema.
55+
*/
56+
public String title;
2657

2758
/**
28-
* The 'description' string without escape charcters needed by some programming languages/targets
59+
* The 'description' string without escape characters needed by some programming languages/targets
2960
*/
3061
public String unescapedDescription;
3162

@@ -47,10 +78,30 @@ public class CodegenProperty implements Cloneable, IJsonSchemaValidationProperti
4778
public String example;
4879

4980
public String jsonSchema;
81+
/**
82+
* The value of the 'minimum' attribute in the OpenAPI schema.
83+
* The value of "minimum" MUST be a number, representing an inclusive lower limit for a numeric instance.
84+
*/
5085
public String minimum;
86+
/**
87+
* The value of the 'maximum' attribute in the OpenAPI schema.
88+
* The value of "maximum" MUST be a number, representing an inclusive upper limit for a numeric instance.
89+
*/
5190
public String maximum;
91+
/**
92+
* The value of the 'multipleOf' attribute in the OpenAPI schema.
93+
* The value of "multipleOf" MUST be a number, strictly greater than 0.
94+
*/
5295
public Number multipleOf;
96+
/**
97+
* The value of the 'exclusiveMinimum' attribute in the OpenAPI schema.
98+
* The value of "exclusiveMinimum" MUST be number, representing an exclusive lower limit for a numeric instance.
99+
*/
53100
public boolean exclusiveMinimum;
101+
/**
102+
* The value of the 'exclusiveMaximum' attribute in the OpenAPI schema.
103+
* The value of "exclusiveMaximum" MUST be number, representing an exclusive upper limit for a numeric instance.
104+
*/
54105
public boolean exclusiveMaximum;
55106
public boolean hasMore;
56107
public boolean required;
@@ -59,6 +110,12 @@ public class CodegenProperty implements Cloneable, IJsonSchemaValidationProperti
59110
public boolean hasMoreNonReadOnly; // for model constructor, true if next property is not readonly
60111
public boolean isPrimitiveType;
61112
public boolean isModel;
113+
/**
114+
* True if this property is an array of items or a map container.
115+
* See:
116+
* - ModelUtils.isArraySchema()
117+
* - ModelUtils.isMapSchema()
118+
*/
62119
public boolean isContainer;
63120
public boolean isString;
64121
public boolean isNumeric;
@@ -76,7 +133,15 @@ public class CodegenProperty implements Cloneable, IJsonSchemaValidationProperti
76133
public boolean isUuid;
77134
public boolean isUri;
78135
public boolean isEmail;
136+
/**
137+
* The type is a free-form object, i.e. it is a map of string to values with no declared properties
138+
*/
79139
public boolean isFreeFormObject;
140+
/**
141+
* The 'type' in the OAS schema is unspecified (i.e. not set). The value can be number, integer, string, object or array.
142+
* If the nullable attribute is set to true, the 'null' value is valid.
143+
*/
144+
public boolean isAnyType;
80145
public boolean isListContainer;
81146
public boolean isMapContainer;
82147
public boolean isEnum;
@@ -621,7 +686,7 @@ public boolean equals(Object o) {
621686
exclusiveMaximum == that.exclusiveMaximum &&
622687
hasMore == that.hasMore &&
623688
required == that.required &&
624-
deprecated == this.deprecated &&
689+
deprecated == that.deprecated &&
625690
secondaryParam == that.secondaryParam &&
626691
hasMoreNonReadOnly == that.hasMoreNonReadOnly &&
627692
isPrimitiveType == that.isPrimitiveType &&

modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1945,14 +1945,19 @@ private String getPrimitiveType(Schema schema) {
19451945
}
19461946
return "string";
19471947
} else if (ModelUtils.isFreeFormObject(schema)) {
1948+
// Note: the value of a free-form object cannot be an arbitrary type. Per OAS specification,
1949+
// it must be a map of string to values.
19481950
return "object";
19491951
} else if (schema.getProperties() != null && !schema.getProperties().isEmpty()) { // having property implies it's a model
19501952
return "object";
19511953
} else if (StringUtils.isNotEmpty(schema.getType())) {
19521954
LOGGER.warn("Unknown type found in the schema: " + schema.getType());
19531955
return schema.getType();
19541956
}
1955-
1957+
// The 'type' attribute has not been set in the OAS schema, which means the value
1958+
// can be an arbitrary type, e.g. integer, string, object, array, number...
1959+
// TODO: we should return a different value to distinguish between free-form object
1960+
// and arbitrary type.
19561961
return "object";
19571962
}
19581963

@@ -2707,6 +2712,9 @@ public CodegenProperty fromProperty(String name, Schema p) {
27072712
setNonArrayMapProperty(property, type);
27082713
Schema refOrCurrent = ModelUtils.getReferencedSchema(this.openAPI, p);
27092714
property.isModel = (ModelUtils.isComposedSchema(refOrCurrent) || ModelUtils.isObjectSchema(refOrCurrent)) && ModelUtils.isModel(refOrCurrent);
2715+
if (ModelUtils.isAnyTypeSchema(p)) {
2716+
property.isAnyType = true;
2717+
}
27102718
}
27112719

27122720
LOGGER.debug("debugging from property return: " + property);

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractGoCodegen.java

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,10 @@ public AbstractGoCodegen() {
9191
"complex64",
9292
"complex128",
9393
"rune",
94-
"byte")
94+
"byte",
95+
"map[string]interface{}",
96+
"interface{}"
97+
)
9598
);
9699

97100
instantiationTypes.clear();
@@ -116,7 +119,17 @@ public AbstractGoCodegen() {
116119
typeMapping.put("file", "*os.File");
117120
typeMapping.put("binary", "*os.File");
118121
typeMapping.put("ByteArray", "string");
122+
// A 'type: object' OAS schema without any declared property is
123+
// (per JSON schema specification) "an unordered set of properties
124+
// mapping a string to an instance".
125+
// Hence map[string]interface{} is the proper implementation in golang.
126+
// Note: OpenAPITools uses the same token 'object' for free-form objects
127+
// and arbitrary types. A free form object is implemented in golang as
128+
// map[string]interface{}, whereas an arbitrary type is implemented
129+
// in golang as interface{}.
130+
// See issue #5387 for more details.
119131
typeMapping.put("object", "map[string]interface{}");
132+
typeMapping.put("interface{}", "interface{}");
120133

121134
numberTypes = new HashSet<String>(
122135
Arrays.asList(
@@ -303,6 +316,12 @@ public String toApiFilename(String name) {
303316
return name;
304317
}
305318

319+
/**
320+
* Return the golang implementation type for the specified property.
321+
*
322+
* @param p the OAS property.
323+
* @return the golang implementation type.
324+
*/
306325
@Override
307326
public String getTypeDeclaration(Schema p) {
308327
if (ModelUtils.isArraySchema(p)) {
@@ -342,6 +361,12 @@ public String getTypeDeclaration(Schema p) {
342361
return toModelName(openAPIType);
343362
}
344363

364+
/**
365+
* Return the OpenAPI type for the property.
366+
*
367+
* @param p the OAS property.
368+
* @return the OpenAPI type.
369+
*/
345370
@Override
346371
public String getSchemaType(Schema p) {
347372
String openAPIType = super.getSchemaType(p);
@@ -350,6 +375,9 @@ public String getSchemaType(Schema p) {
350375

351376
if (ref != null && !ref.isEmpty()) {
352377
type = openAPIType;
378+
} else if ("object".equals(openAPIType) && ModelUtils.isAnyTypeSchema(p)) {
379+
// Arbitrary type. Note this is not the same thing as free-form object.
380+
type = "interface{}";
353381
} else if (typeMapping.containsKey(openAPIType)) {
354382
type = typeMapping.get(openAPIType);
355383
if (languageSpecificPrimitives.contains(type))
@@ -556,6 +584,17 @@ private void setExportParameterName(List<CodegenParameter> codegenParameters) {
556584
}
557585
}
558586

587+
@Override
588+
public void postProcessModelProperty(CodegenModel model, CodegenProperty property) {
589+
// The 'go-experimental/model.mustache' template conditionally generates accessor methods.
590+
// For primitive types and custom types (e.g. interface{}, map[string]interface{}...),
591+
// the generated code has a wrapper type and a Get() function to access the underlying type.
592+
// For containers (e.g. Array, Map), the generated code returns the type directly.
593+
if (property.isContainer || property.isFreeFormObject || property.isAnyType) {
594+
property.vendorExtensions.put("x-golang-is-container", true);
595+
}
596+
}
597+
559598
@Override
560599
public Map<String, Object> postProcessModels(Map<String, Object> objs) {
561600
// remove model imports to avoid error

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/GoClientExperimentalCodegen.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,8 @@ public Map<String, Object> postProcessModels(Map<String, Object> objs) {
167167

168168
for (CodegenProperty param : model.vars) {
169169
param.vendorExtensions.put("x-go-base-type", param.dataType);
170-
if (!param.isNullable || param.isMapContainer || param.isListContainer) {
170+
if (!param.isNullable || param.isMapContainer || param.isListContainer ||
171+
param.isFreeFormObject || param.isAnyType) {
171172
continue;
172173
}
173174
if (param.isDateTime) {

modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -636,7 +636,7 @@ public static boolean isEmailSchema(Schema schema) {
636636
}
637637

638638
/**
639-
* Check to see if the schema is a model with at least one properties
639+
* Check to see if the schema is a model with at least one property.
640640
*
641641
* @param schema potentially containing a '$ref'
642642
* @return true if it's a model with at least one properties
@@ -657,6 +657,42 @@ public static boolean isModel(Schema schema) {
657657
return schema instanceof ComposedSchema;
658658
}
659659

660+
/**
661+
* Return true if the schema value can be any type, i.e. it can be
662+
* the null value, integer, number, string, object or array.
663+
* One use case is when the "type" attribute in the OAS schema is unspecified.
664+
*
665+
* Examples:
666+
*
667+
* arbitraryTypeValue:
668+
* description: This is an arbitrary type schema.
669+
* It is not a free-form object.
670+
* The value can be any type except the 'null' value.
671+
* arbitraryTypeNullableValue:
672+
* description: This is an arbitrary type schema.
673+
* It is not a free-form object.
674+
* The value can be any type, including the 'null' value.
675+
* nullable: true
676+
*
677+
* @param schema the OAS schema.
678+
* @return true if the schema value can be an arbitrary type.
679+
*/
680+
public static boolean isAnyTypeSchema(Schema schema) {
681+
if (schema == null) {
682+
once(LOGGER).error("Schema cannot be null in isAnyTypeSchema check");
683+
return false;
684+
}
685+
if (schema.getClass().equals(Schema.class) && schema.get$ref() == null && schema.getType() == null &&
686+
(schema.getProperties() == null || schema.getProperties().isEmpty()) &&
687+
schema.getAdditionalProperties() == null && schema.getNot() == null &&
688+
schema.getEnum() == null) {
689+
return true;
690+
// If and when type arrays are supported in a future OAS specification,
691+
// we could return true if the type array includes all possible JSON schema types.
692+
}
693+
return false;
694+
}
695+
660696
/**
661697
* Check to see if the schema is a free form object.
662698
*
@@ -665,6 +701,25 @@ public static boolean isModel(Schema schema) {
665701
* 2) Is not a composed schema (no anyOf, oneOf, allOf), and
666702
* 3) additionalproperties is not defined, or additionalproperties: true, or additionalproperties: {}.
667703
*
704+
* Examples:
705+
*
706+
* components:
707+
* schemas:
708+
* arbitraryObject:
709+
* type: object
710+
* description: This is a free-form object.
711+
* The value must be a map of strings to values. The value cannot be 'null'.
712+
* It cannot be array, string, integer, number.
713+
* arbitraryNullableObject:
714+
* type: object
715+
* description: This is a free-form object.
716+
* The value must be a map of strings to values. The value can be 'null',
717+
* It cannot be array, string, integer, number.
718+
* nullable: true
719+
* arbitraryTypeValue:
720+
* description: This is NOT a free-form object.
721+
* The value can be any type except the 'null' value.
722+
*
668723
* @param schema potentially containing a '$ref'
669724
* @return true if it's a free-form object
670725
*/
@@ -1361,4 +1416,4 @@ public static void syncValidationProperties(Schema schema, IJsonSchemaValidation
13611416
if (maxProperties != null) target.setMaxProperties(maxProperties);
13621417
}
13631418
}
1364-
}
1419+
}

0 commit comments

Comments
 (0)