From 3bbaedd9bc086028e7b1be973a30e04870962e5d Mon Sep 17 00:00:00 2001 From: William Cheng Date: Thu, 30 Apr 2020 21:23:55 +0800 Subject: [PATCH] Better "Any Type" support (#6091) * better anytype support * add tests for any type * fix test with any_value * fix tests * fix case additionalProperties: {} * test with CI * remove check in map schema * Revert "remove check in map schema" This reverts commit e016c4155f1bd8753e894d87bad8e70eee29f3d5. * fix tests, comment out map schema fix * fix tests * fix tests with correct codegen model * fix tests * fix tests for map of any type * fix array of any type * fix array of any type * update samples, remove log * add typemapping to go, python --- .../codegen/CodegenParameter.java | 7 +- .../openapitools/codegen/CodegenResponse.java | 5 +- .../openapitools/codegen/DefaultCodegen.java | 182 +++++------------- .../codegen/languages/AbstractGoCodegen.java | 1 + .../languages/PythonClientCodegen.java | 1 + .../codegen/utils/ModelUtils.java | 39 ++-- .../codegen/java/JavaClientCodegenTest.java | 145 +++++++++++++- .../src/test/resources/3_0/any_type.yaml | 59 ++++++ .../src/test/resources/3_0/issue796.yaml | 11 +- .../go-experimental/go-petstore/docs/User.md | 4 +- 10 files changed, 302 insertions(+), 152 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/any_type.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenParameter.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenParameter.java index 7072df85be8a..fda66803235c 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenParameter.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenParameter.java @@ -34,7 +34,7 @@ public class CodegenParameter implements IJsonSchemaValidationProperties { public String example; // example value (x-example) public String jsonSchema; public boolean isString, isNumeric, isInteger, isLong, isNumber, isFloat, isDouble, isByteArray, isBinary, - isBoolean, isDate, isDateTime, isUuid, isUri, isEmail, isFreeFormObject; + isBoolean, isDate, isDateTime, isUuid, isUri, isEmail, isFreeFormObject, isAnyType; public boolean isListContainer, isMapContainer; public boolean isFile; public boolean isEnum; @@ -178,6 +178,7 @@ public CodegenParameter copy() { output.isUri = this.isUri; output.isEmail = this.isEmail; output.isFreeFormObject = this.isFreeFormObject; + output.isAnyType = this.isAnyType; output.isListContainer = this.isListContainer; output.isMapContainer = this.isMapContainer; output.isExplode = this.isExplode; @@ -188,7 +189,7 @@ public CodegenParameter copy() { @Override public int hashCode() { - return Objects.hash(isFormParam, isQueryParam, isPathParam, isHeaderParam, isCookieParam, isBodyParam, hasMore, isContainer, secondaryParam, isCollectionFormatMulti, isPrimitiveType, isModel, isExplode, baseName, paramName, dataType, datatypeWithEnum, dataFormat, collectionFormat, description, unescapedDescription, baseType, defaultValue, enumName, style, example, jsonSchema, isString, isNumeric, isInteger, isLong, isNumber, isFloat, isDouble, isByteArray, isBinary, isBoolean, isDate, isDateTime, isUuid, isUri, isEmail, isFreeFormObject, isListContainer, isMapContainer, isFile, isEnum, _enum, allowableValues, items, mostInnerItems, vendorExtensions, hasValidation, getMaxProperties(), getMinProperties(), isNullable, required, getMaximum(), getExclusiveMaximum(), getMinimum(), getExclusiveMinimum(), getMaxLength(), getMinLength(), getPattern(), getMaxItems(), getMinItems(), getUniqueItems(), multipleOf); + return Objects.hash(isFormParam, isQueryParam, isPathParam, isHeaderParam, isCookieParam, isBodyParam, hasMore, isContainer, secondaryParam, isCollectionFormatMulti, isPrimitiveType, isModel, isExplode, baseName, paramName, dataType, datatypeWithEnum, dataFormat, collectionFormat, description, unescapedDescription, baseType, defaultValue, enumName, style, example, jsonSchema, isString, isNumeric, isInteger, isLong, isNumber, isFloat, isDouble, isByteArray, isBinary, isBoolean, isDate, isDateTime, isUuid, isUri, isEmail, isFreeFormObject, isAnyType, isListContainer, isMapContainer, isFile, isEnum, _enum, allowableValues, items, mostInnerItems, vendorExtensions, hasValidation, getMaxProperties(), getMinProperties(), isNullable, required, getMaximum(), getExclusiveMaximum(), getMinimum(), getExclusiveMinimum(), getMaxLength(), getMinLength(), getPattern(), getMaxItems(), getMinItems(), getUniqueItems(), multipleOf); } @Override @@ -225,6 +226,7 @@ public boolean equals(Object o) { isUri == that.isUri && isEmail == that.isEmail && isFreeFormObject == that.isFreeFormObject && + isAnyType == that.isAnyType && isListContainer == that.isListContainer && isMapContainer == that.isMapContainer && isFile == that.isFile && @@ -312,6 +314,7 @@ public String toString() { sb.append(", isUri=").append(isUri); sb.append(", isEmail=").append(isEmail); sb.append(", isFreeFormObject=").append(isFreeFormObject); + sb.append(", isAnyType=").append(isAnyType); sb.append(", isListContainer=").append(isListContainer); sb.append(", isMapContainer=").append(isMapContainer); sb.append(", isFile=").append(isFile); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenResponse.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenResponse.java index 1edad97316f6..9e9d28958af3 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenResponse.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenResponse.java @@ -49,6 +49,7 @@ public class CodegenResponse implements IJsonSchemaValidationProperties { public boolean isEmail; public boolean isModel; public boolean isFreeFormObject; + public boolean isAnyType; public boolean isDefault; public boolean simpleType; public boolean primitiveType; @@ -77,7 +78,7 @@ public class CodegenResponse implements IJsonSchemaValidationProperties { public int hashCode() { return Objects.hash(headers, code, message, hasMore, examples, dataType, baseType, containerType, hasHeaders, isString, isNumeric, isInteger, isLong, isNumber, isFloat, isDouble, isByteArray, isBoolean, isDate, - isDateTime, isUuid, isEmail, isModel, isFreeFormObject, isDefault, simpleType, primitiveType, + isDateTime, isUuid, isEmail, isModel, isFreeFormObject, isAnyType, isDefault, simpleType, primitiveType, isMapContainer, isListContainer, isBinary, isFile, schema, jsonSchema, vendorExtensions, getMaxProperties(), getMinProperties(), uniqueItems, getMaxItems(), getMinItems(), getMaxLength(), getMinLength(), exclusiveMinimum, exclusiveMaximum, getMinimum(), getMaximum(), getPattern()); @@ -105,6 +106,7 @@ public boolean equals(Object o) { isEmail == that.isEmail && isModel == that.isModel && isFreeFormObject == that.isFreeFormObject && + isAnyType == that.isAnyType && isDefault == that.isDefault && simpleType == that.simpleType && primitiveType == that.primitiveType && @@ -295,6 +297,7 @@ public String toString() { sb.append(", isEmail=").append(isEmail); sb.append(", isModel=").append(isModel); sb.append(", isFreeFormObject=").append(isFreeFormObject); + sb.append(", isAnyType=").append(isAnyType); sb.append(", isDefault=").append(isDefault); sb.append(", simpleType=").append(simpleType); sb.append(", primitiveType=").append(primitiveType); 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 d4cc25414375..2480fa24fc92 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 @@ -1122,7 +1122,7 @@ public Boolean getLegacyDiscriminatorBehavior() { return legacyDiscriminatorBehavior; } - public void setLegacyDiscriminatorBehavior(boolean val){ + public void setLegacyDiscriminatorBehavior(boolean val) { this.legacyDiscriminatorBehavior = val; } @@ -1398,6 +1398,7 @@ public DefaultCodegen() { typeMapping.put("UUID", "UUID"); typeMapping.put("URI", "URI"); typeMapping.put("BigDecimal", "BigDecimal"); + typeMapping.put("AnyType", "oas_any_type_not_mapped"); instantiationTypes = new HashMap(); @@ -1863,9 +1864,9 @@ public String toAnyOfName(List names, ComposedSchema composedSchema) { /** * Return the name of the oneOf schema. - * + *

* This name is used to set the value of CodegenProperty.openApiType. - * + *

* If the 'x-one-of-name' extension is specified in the OAS document, return that value. * Otherwise, a name is constructed by creating a comma-separated list of all the names * of the oneOf schemas. @@ -1981,6 +1982,8 @@ private String getPrimitiveType(Schema schema) { return "object"; } else if (schema.getProperties() != null && !schema.getProperties().isEmpty()) { // having property implies it's a model return "object"; + } else if (ModelUtils.isAnyTypeSchema(schema)) { + return "AnyType"; } else if (StringUtils.isNotEmpty(schema.getType())) { LOGGER.warn("Unknown type found in the schema: " + schema.getType()); return schema.getType(); @@ -2398,8 +2401,8 @@ public CodegenModel fromModel(String name, Schema schema) { listOLists.add(m.requiredVars); listOLists.add(m.vars); listOLists.add(m.allVars); - for (List theseVars: listOLists) { - for (CodegenProperty requiredVar: theseVars) { + for (List theseVars : listOLists) { + for (CodegenProperty requiredVar : theseVars) { if (discPropName.equals(requiredVar.baseName)) { requiredVar.isDiscriminator = true; } @@ -2427,9 +2430,10 @@ public int compare(CodegenProperty one, CodegenProperty another) { * Recursively look in Schema sc for the discriminator discPropName * and return a CodegenProperty with the dataType and required params set * the returned CodegenProperty may not be required and it may not be of type string + * * @param composedSchemaName The name of the sc Schema - * @param sc The Schema that may contain the discriminator - * @param discPropName The String that is the discriminator propertyName in the schema + * @param sc The Schema that may contain the discriminator + * @param discPropName The String that is the discriminator propertyName in the schema */ private CodegenProperty discriminatorFound(String composedSchemaName, Schema sc, String discPropName, OpenAPI openAPI) { Schema refSchema = ModelUtils.getReferencedSchema(openAPI, sc); @@ -2449,7 +2453,7 @@ private CodegenProperty discriminatorFound(String composedSchemaName, Schema sc, ComposedSchema composedSchema = (ComposedSchema) refSchema; if (composedSchema.getAllOf() != null) { // If our discriminator is in one of the allOf schemas break when we find it - for (Schema allOf: composedSchema.getAllOf()) { + for (Schema allOf : composedSchema.getAllOf()) { CodegenProperty cp = discriminatorFound(composedSchemaName, allOf, discPropName, openAPI); if (cp != null) { return cp; @@ -2459,18 +2463,18 @@ private CodegenProperty discriminatorFound(String composedSchemaName, Schema sc, if (composedSchema.getOneOf() != null && composedSchema.getOneOf().size() != 0) { // All oneOf definitions must contain the discriminator CodegenProperty cp = new CodegenProperty(); - for (Schema oneOf: composedSchema.getOneOf()) { + for (Schema oneOf : composedSchema.getOneOf()) { String modelName = ModelUtils.getSimpleRef(oneOf.get$ref()); CodegenProperty thisCp = discriminatorFound(composedSchemaName, oneOf, discPropName, openAPI); if (thisCp == null) { - throw new RuntimeException("'" + composedSchemaName + "' defines discriminator '" + discPropName + "', but the referenced OneOf schema '" + modelName + "' is missing "+discPropName); + throw new RuntimeException("'" + composedSchemaName + "' defines discriminator '" + discPropName + "', but the referenced OneOf schema '" + modelName + "' is missing " + discPropName); } if (cp.dataType == null) { cp = thisCp; continue; } if (cp != thisCp) { - throw new RuntimeException("'" + composedSchemaName + "' defines discriminator '" + discPropName + "', but the OneOf schema '" + modelName + "' has a different "+discPropName+" definition than the prior OneOf schema's. Make sure the "+discPropName+" type and required values are the same"); + throw new RuntimeException("'" + composedSchemaName + "' defines discriminator '" + discPropName + "', but the OneOf schema '" + modelName + "' has a different " + discPropName + " definition than the prior OneOf schema's. Make sure the " + discPropName + " type and required values are the same"); } } return cp; @@ -2478,18 +2482,18 @@ private CodegenProperty discriminatorFound(String composedSchemaName, Schema sc, if (composedSchema.getAnyOf() != null && composedSchema.getAnyOf().size() != 0) { // All anyOf definitions must contain the discriminator because a min of one must be selected CodegenProperty cp = new CodegenProperty(); - for (Schema anyOf: composedSchema.getAnyOf()) { + for (Schema anyOf : composedSchema.getAnyOf()) { String modelName = ModelUtils.getSimpleRef(anyOf.get$ref()); CodegenProperty thisCp = discriminatorFound(composedSchemaName, anyOf, discPropName, openAPI); if (thisCp == null) { - throw new RuntimeException("'" + composedSchemaName + "' defines discriminator '" + discPropName + "', but the referenced AnyOf schema '" + modelName + "' is missing "+discPropName); + throw new RuntimeException("'" + composedSchemaName + "' defines discriminator '" + discPropName + "', but the referenced AnyOf schema '" + modelName + "' is missing " + discPropName); } if (cp.dataType == null) { cp = thisCp; continue; } if (cp != thisCp) { - throw new RuntimeException("'" + composedSchemaName + "' defines discriminator '" + discPropName + "', but the AnyOf schema '" + modelName + "' has a different "+discPropName+" definition than the prior AnyOf schema's. Make sure the "+discPropName+" type and required values are the same"); + throw new RuntimeException("'" + composedSchemaName + "' defines discriminator '" + discPropName + "', but the AnyOf schema '" + modelName + "' has a different " + discPropName + " definition than the prior AnyOf schema's. Make sure the " + discPropName + " type and required values are the same"); } } return cp; @@ -2503,6 +2507,7 @@ private CodegenProperty discriminatorFound(String composedSchemaName, Schema sc, * Recursively look in Schema sc for the discriminator and return it * Schema sc location * OpenAPI openAPI the spec where we are searching for the discriminator + * * @param sc The Schema that may contain the discriminator */ private Discriminator recursiveGetDiscriminator(Schema sc, OpenAPI openAPI) { @@ -2519,7 +2524,7 @@ private Discriminator recursiveGetDiscriminator(Schema sc, OpenAPI openAPI) { ComposedSchema composedSchema = (ComposedSchema) refSchema; if (composedSchema.getAllOf() != null) { // If our discriminator is in one of the allOf schemas break when we find it - for (Schema allOf: composedSchema.getAllOf()) { + for (Schema allOf : composedSchema.getAllOf()) { foundDisc = recursiveGetDiscriminator(allOf, openAPI); if (foundDisc != null) { disc.setPropertyName(foundDisc.getPropertyName()); @@ -2532,7 +2537,7 @@ private Discriminator recursiveGetDiscriminator(Schema sc, OpenAPI openAPI) { // All oneOf definitions must contain the discriminator Integer hasDiscriminatorCnt = 0; Set discriminatorsPropNames = new HashSet<>(); - for (Schema oneOf: composedSchema.getOneOf()) { + for (Schema oneOf : composedSchema.getOneOf()) { foundDisc = recursiveGetDiscriminator(oneOf, openAPI); if (foundDisc != null) { discriminatorsPropNames.add(foundDisc.getPropertyName()); @@ -2549,7 +2554,7 @@ private Discriminator recursiveGetDiscriminator(Schema sc, OpenAPI openAPI) { // All anyOf definitions must contain the discriminator because a min of one must be selected Integer hasDiscriminatorCnt = 0; Set discriminatorsPropNames = new HashSet<>(); - for (Schema anyOf: composedSchema.getAnyOf()) { + for (Schema anyOf : composedSchema.getAnyOf()) { foundDisc = recursiveGetDiscriminator(anyOf, openAPI); if (foundDisc != null) { discriminatorsPropNames.add(foundDisc.getPropertyName()); @@ -2573,10 +2578,11 @@ private Discriminator recursiveGetDiscriminator(Schema sc, OpenAPI openAPI) { * the required discriminator. If they don't contain the required * discriminator or the discriminator is the wrong type then an error is * thrown + * * @param composedSchemaName The String model name of the composed schema where we are setting the discriminator map - * @param discPropName The String that is the discriminator propertyName in the schema - * @param c The ComposedSchema that contains the discriminator and oneOf/anyOf schemas - * @param openAPI The OpenAPI spec that we are using + * @param discPropName The String that is the discriminator propertyName in the schema + * @param c The ComposedSchema that contains the discriminator and oneOf/anyOf schemas + * @param openAPI The OpenAPI spec that we are using * @return the list of oneOf and anyOf MappedModel that need to be added to the discriminator map */ protected List getOneOfAnyOfDescendants(String composedSchemaName, String discPropName, ComposedSchema c, OpenAPI openAPI) { @@ -2584,11 +2590,11 @@ protected List getOneOfAnyOfDescendants(String composedSchemaName, listOLists.add(c.getOneOf()); listOLists.add(c.getAnyOf()); List descendentSchemas = new ArrayList(); - for (List schemaList: listOLists) { + for (List schemaList : listOLists) { if (schemaList == null) { continue; } - for (Schema sc: schemaList) { + for (Schema sc : schemaList) { String ref = sc.get$ref(); if (ref == null) { // for schemas with no ref, it is not possible to build the discriminator map @@ -2604,20 +2610,20 @@ protected List getOneOfAnyOfDescendants(String composedSchemaName, if (df == null || !df.isString || df.required != true) { String msgSuffix = ""; if (df == null) { - msgSuffix += discPropName+" is missing from the schema, define it as required and type string"; + msgSuffix += discPropName + " is missing from the schema, define it as required and type string"; } else { if (!df.isString) { - msgSuffix += "invalid type for "+discPropName+", set it to string"; + msgSuffix += "invalid type for " + discPropName + ", set it to string"; } if (df.required != true) { String spacer = ""; if (msgSuffix.length() != 0) { spacer = ". "; } - msgSuffix += spacer+"invalid optional definition of "+discPropName+", include it in required"; + msgSuffix += spacer + "invalid optional definition of " + discPropName + ", include it in required"; } } - throw new RuntimeException("'" + composedSchemaName + "' defines discriminator '" + discPropName + "', but the referenced schema '" + modelName + "' is incorrect. "+msgSuffix); + throw new RuntimeException("'" + composedSchemaName + "' defines discriminator '" + discPropName + "', but the referenced schema '" + modelName + "' is incorrect. " + msgSuffix); } MappedModel mm = new MappedModel(modelName, toModelName(modelName)); descendentSchemas.add(mm); @@ -2635,7 +2641,7 @@ protected List getOneOfAnyOfDescendants(String composedSchemaName, } protected List getAllOfDescendants(String thisSchemaName, OpenAPI openAPI) { - ArrayList queue = new ArrayList();; + ArrayList queue = new ArrayList(); List descendentSchemas = new ArrayList(); Map schemas = ModelUtils.getSchemas(openAPI); String currentSchemaName = thisSchemaName; @@ -2649,7 +2655,7 @@ protected List getAllOfDescendants(String thisSchemaName, OpenAPI o ComposedSchema composedChild = (ComposedSchema) child; List parents = composedChild.getAllOf(); if (parents != null) { - for (Schema parent: parents) { + for (Schema parent : parents) { String ref = parent.get$ref(); if (ref == null) { // for schemas with no ref, it is not possible to build the discriminator map @@ -2661,7 +2667,7 @@ protected List getAllOfDescendants(String thisSchemaName, OpenAPI o String parentName = ModelUtils.getSimpleRef(ref); if (parentName.equals(currentSchemaName)) { if (queue.contains(childName) || descendentSchemas.contains(childName)) { - throw new RuntimeException("Stack overflow hit when looking for "+thisSchemaName+" an infinite loop starting and ending at "+childName+" was seen"); + throw new RuntimeException("Stack overflow hit when looking for " + thisSchemaName + " an infinite loop starting and ending at " + childName + " was seen"); } queue.add(childName); break; @@ -2714,7 +2720,7 @@ protected CodegenDiscriminator createDiscriminator(String schemaName, Schema sch if (!this.getLegacyDiscriminatorBehavior() || legacyUseCase) { // for schemas that allOf inherit from this schema, add those descendants to this discriminator map List otherDescendants = getAllOfDescendants(schemaName, openAPI); - for (MappedModel otherDescendant: otherDescendants) { + for (MappedModel otherDescendant : otherDescendants) { if (!uniqueDescendants.contains(otherDescendant)) { uniqueDescendants.add(otherDescendant); } @@ -2723,7 +2729,7 @@ protected CodegenDiscriminator createDiscriminator(String schemaName, Schema sch // if there are composed oneOf/anyOf schemas, add them to this discriminator if (ModelUtils.isComposedSchema(schema) && !this.getLegacyDiscriminatorBehavior()) { List otherDescendants = getOneOfAnyOfDescendants(schemaName, discPropName, (ComposedSchema) schema, openAPI); - for (MappedModel otherDescendant: otherDescendants) { + for (MappedModel otherDescendant : otherDescendants) { if (!uniqueDescendants.contains(otherDescendant)) { uniqueDescendants.add(otherDescendant); } @@ -2968,6 +2974,8 @@ public CodegenProperty fromProperty(String name, Schema p) { } else if (ModelUtils.isFreeFormObject(p)) { property.isFreeFormObject = true; + } else if (ModelUtils.isAnyTypeSchema(p)) { + property.isAnyType = true; } else if (ModelUtils.isArraySchema(p)) { // default to string if inner item is undefined ArraySchema arraySchema = (ArraySchema) p; @@ -3074,18 +3082,13 @@ public CodegenProperty fromProperty(String name, Schema p) { } else if (ModelUtils.isFreeFormObject(p)) { property.isFreeFormObject = true; property.baseType = getSchemaType(p); + } else if (ModelUtils.isAnyTypeSchema(p)) { + property.isAnyType = true; + property.baseType = getSchemaType(p); } else { // model - // TODO revise the logic below - //if (StringUtils.isNotBlank(p.get$ref())) { - // property.baseType = getSimpleRef(p.get$ref()); - //} - // --END of revision setNonArrayMapProperty(property, type); Schema refOrCurrent = ModelUtils.getReferencedSchema(this.openAPI, p); property.isModel = (ModelUtils.isComposedSchema(refOrCurrent) || ModelUtils.isObjectSchema(refOrCurrent)) && ModelUtils.isModel(refOrCurrent); - if (ModelUtils.isAnyTypeSchema(p)) { - property.isAnyType = true; - } } LOGGER.debug("debugging from property return: " + property); @@ -3773,6 +3776,8 @@ public CodegenResponse fromResponse(String responseCode, ApiResponse response) { r.isDateTime = true; } else if (Boolean.TRUE.equals(cp.isFreeFormObject)) { r.isFreeFormObject = true; + } else if (Boolean.TRUE.equals(cp.isAnyType)) { + r.isAnyType = true; } else { LOGGER.debug("Property type is not primitive: " + cp.dataType); } @@ -4074,91 +4079,6 @@ public CodegenParameter fromParameter(Parameter parameter, Set imports) } else { LOGGER.error("ERROR! Not handling " + parameter + " as Body Parameter at the moment"); - /* TODO need to revise the logic below to handle body parameter - if (!(parameter instanceof BodyParameter)) { - LOGGER.error("Cannot use Parameter " + parameter + " as Body Parameter"); - } - - BodyParameter bp = (BodyParameter) param; - Model model = bp.getSchema(); - - if (model instanceof ModelImpl) { - ModelImpl impl = (ModelImpl) model; - CodegenModel cm = fromModel(bp.getName(), impl); - if (!cm.emptyVars) { - codegen.dataType = getTypeDeclaration(cm.classname); - imports.add(p.dataType); - } else { - Property prop = PropertyBuilder.build(impl.getType(), impl.getFormat(), null); - prop.setRequired(bp.getRequired()); - CodegenProperty cp = fromProperty("property", prop); - if (cp != null) { - p.baseType = cp.baseType; - p.dataType = cp.datatype; - p.isPrimitiveType = cp.isPrimitiveType; - p.isBinary = isDataTypeBinary(cp.datatype); - p.isFile = isDataTypeFile(cp.datatype); - if (cp.complexType != null) { - imports.add(cp.complexType); - } - } - - // set boolean flag (e.g. isString) - setParameterBooleanFlagWithCodegenProperty(p, cp); - } - } else if (model instanceof ArrayModel) { - // to use the built-in model parsing, we unwrap the ArrayModel - // and get a single property from it - ArrayModel impl = (ArrayModel) model; - // get the single property - ArrayProperty ap = new ArrayProperty().items(impl.getItems()); - ap.setRequired(param.getRequired()); - CodegenProperty cp = fromProperty("inner", ap); - if (cp.complexType != null) { - imports.add(cp.complexType); - } - imports.add(cp.baseType); - - // recursively add import - CodegenProperty innerCp = cp; - while(innerCp != null) { - if(innerCp.complexType != null) { - imports.add(innerCp.complexType); - } - innerCp = innerCp.items; - } - - p.items = cp; - p.dataType = cp.datatype; - p.baseType = cp.complexType; - p.isPrimitiveType = cp.isPrimitiveType; - p.isContainer = true; - p.isListContainer = true; - - // set boolean flag (e.g. isString) - setParameterBooleanFlagWithCodegenProperty(p, cp); - } else { - Model sub = bp.getSchema(); - if (sub instanceof RefModel) { - String name = ((RefModel) sub).getSimpleRef(); - name = getAlias(name); - if (typeMapping.containsKey(name)) { - name = typeMapping.get(name); - p.baseType = name; - } else { - name = toModelName(name); - p.baseType = name; - if (defaultIncludes.contains(name)) { - imports.add(name); - } - imports.add(name); - name = getTypeDeclaration(name); - } - p.dataType = name; - } - } - p.paramName = toParamName(bp.getName()); - */ } if (parameter instanceof QueryParameter || "query".equalsIgnoreCase(parameter.getIn())) { @@ -5128,6 +5048,8 @@ public void setParameterBooleanFlagWithCodegenProperty(CodegenParameter paramete parameter.isPrimitiveType = true; } else if (Boolean.TRUE.equals(property.isFreeFormObject)) { parameter.isFreeFormObject = true; + } else if (Boolean.TRUE.equals(property.isAnyType)) { + parameter.isAnyType = true; } else { LOGGER.debug("Property type is not primitive: " + property.dataType); } @@ -5702,7 +5624,7 @@ private void addBodyModelSchema(CodegenParameter codegenParameter, String name, codegenParameter.dataType = getTypeDeclaration(codegenModelName); codegenParameter.description = codegenProperty.getDescription(); } else { - if (ModelUtils.getAdditionalProperties(schema) != null) {// http body is map + if (ModelUtils.isMapSchema(schema)) {// http body is map LOGGER.error("Map should be supported. Please report to openapi-generator github repo about the issue."); } else if (codegenProperty != null) { String codegenModelName, codegenModelDescription; @@ -5878,7 +5800,7 @@ public CodegenParameter fromRequestBody(RequestBody body, Set imports, S setParameterNullable(codegenParameter, codegenProperty); } else if (ModelUtils.isObjectSchema(schema) || ModelUtils.isComposedSchema(schema)) { - this.addBodyModelSchema(codegenParameter, name, schema, imports, bodyParameterName, false); + this.addBodyModelSchema(codegenParameter, name, schema, imports, bodyParameterName, false); } else { // HTTP request body is primitive type (e.g. integer, string, etc) CodegenProperty codegenProperty = fromProperty("PRIMITIVE_REQUEST_BODY", schema); @@ -5920,8 +5842,8 @@ public CodegenParameter fromRequestBody(RequestBody body, Set imports, S return codegenParameter; } - private void addJsonSchemaForBodyRequestInCaseItsNotPresent(CodegenParameter codegenParameter, RequestBody body){ - if(codegenParameter.jsonSchema == null) + private void addJsonSchemaForBodyRequestInCaseItsNotPresent(CodegenParameter codegenParameter, RequestBody body) { + if (codegenParameter.jsonSchema == null) codegenParameter.jsonSchema = Json.pretty(body); } @@ -6166,8 +6088,8 @@ public void addOneOfNameExtension(ComposedSchema s, String name) { /** * Add a given ComposedSchema as an interface model to be generated, assuming it has `oneOf` defined * - * @param cs ComposedSchema object to create as interface model - * @param type name to use for the generated interface model + * @param cs ComposedSchema object to create as interface model + * @param type name to use for the generated interface model * @param openAPI OpenAPI spec that we are using */ public void addOneOfInterfaceModel(ComposedSchema cs, String type, OpenAPI openAPI) { diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractGoCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractGoCodegen.java index d4c0a8514e6e..b018d22707a6 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractGoCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractGoCodegen.java @@ -130,6 +130,7 @@ public AbstractGoCodegen() { // See issue #5387 for more details. typeMapping.put("object", "map[string]interface{}"); typeMapping.put("interface{}", "interface{}"); + typeMapping.put("AnyType", "interface{}"); numberTypes = new HashSet( Arrays.asList( diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java index 077676af99b0..74796111afd9 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java @@ -134,6 +134,7 @@ public PythonClientCodegen() { typeMapping.put("date", "date"); typeMapping.put("DateTime", "datetime"); typeMapping.put("object", "object"); + typeMapping.put("AnyType", "object"); typeMapping.put("file", "file"); // TODO binary should be mapped to byte array // mapped to String as a workaround diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java index 6dbefe36db49..7092f2961715 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java @@ -118,7 +118,7 @@ public static List getAllUsedSchemas(OpenAPI openAPI) { * @return schemas a list of unused schemas */ public static List getUnusedSchemas(OpenAPI openAPI) { - final Map> childrenMap; + final Map> childrenMap; Map> tmpChildrenMap; try { tmpChildrenMap = getChildrenMap(openAPI); @@ -661,7 +661,7 @@ public static boolean isModel(Schema schema) { * Return true if the schema value can be any type, i.e. it can be * the null value, integer, number, string, object or array. * One use case is when the "type" attribute in the OAS schema is unspecified. - * + * * Examples: * * arbitraryTypeValue: @@ -675,13 +675,19 @@ public static boolean isModel(Schema schema) { * nullable: true * * @param schema the OAS schema. - * @return true if the schema value can be an arbitrary type. + * @return true if the schema value can be an arbitrary type. */ public static boolean isAnyTypeSchema(Schema schema) { if (schema == null) { once(LOGGER).error("Schema cannot be null in isAnyTypeSchema check"); return false; } + + if (isFreeFormObject(schema)) { + // make sure it's not free form object + return false; + } + if (schema.getClass().equals(Schema.class) && schema.get$ref() == null && schema.getType() == null && (schema.getProperties() == null || schema.getProperties().isEmpty()) && schema.getAdditionalProperties() == null && schema.getNot() == null && @@ -702,7 +708,7 @@ public static boolean isAnyTypeSchema(Schema schema) { * 3) additionalproperties is not defined, or additionalproperties: true, or additionalproperties: {}. * * Examples: - * + * * components: * schemas: * arbitraryObject: @@ -719,7 +725,7 @@ public static boolean isAnyTypeSchema(Schema schema) { * arbitraryTypeValue: * description: This is NOT a free-form object. * The value can be any type except the 'null' value. - * + * * @param schema potentially containing a '$ref' * @return true if it's a free-form object */ @@ -754,6 +760,11 @@ public static boolean isFreeFormObject(Schema schema) { if (objSchema.getProperties() == null || objSchema.getProperties().isEmpty()) { return true; } + } else if (addlProps instanceof Schema) { + // additionalProperties defined as {} + if (addlProps.getType() == null && (addlProps.getProperties() == null || addlProps.getProperties().isEmpty())) { + return true; + } } } } @@ -986,7 +997,7 @@ private static Schema getSchemaFromContent(Content content) { // Other content types are currently ignored by codegen. If you see this warning, // reorder the OAS spec to put the desired content type first. once(LOGGER).warn("Multiple schemas found in the OAS 'content' section, returning only the first one ({})", - entry.getKey()); + entry.getKey()); } return entry.getValue().getSchema(); } @@ -1105,9 +1116,9 @@ public static Map> getChildrenMap(OpenAPI openAPI) { Map allSchemas = getSchemas(openAPI); Map>> groupedByParent = allSchemas.entrySet().stream() - .filter(entry -> isComposedSchema(entry.getValue())) + .filter(entry -> isComposedSchema(entry.getValue())) .filter(entry -> getParentName((ComposedSchema) entry.getValue(), allSchemas)!=null) - .collect(Collectors.groupingBy(entry -> getParentName((ComposedSchema) entry.getValue(), allSchemas))); + .collect(Collectors.groupingBy(entry -> getParentName((ComposedSchema) entry.getValue(), allSchemas))); return groupedByParent.entrySet().stream() .collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue().stream().map(e -> e.getKey()).collect(Collectors.toList()))); @@ -1205,8 +1216,8 @@ public static String getParentName(ComposedSchema composedSchema, Map interfaces = getInterfaces(composed); for (Schema i : interfaces) { @@ -1376,7 +1385,7 @@ public static boolean isNullType(Schema schema) { return false; } - public static void syncValidationProperties(Schema schema, IJsonSchemaValidationProperties target){ + public static void syncValidationProperties(Schema schema, IJsonSchemaValidationProperties target) { if (schema != null && target != null) { target.setPattern(schema.getPattern()); BigDecimal minimum = schema.getMinimum(); 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 38b75b750abe..fa3f2b152487 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 @@ -505,7 +505,7 @@ public void testFreeFormObjects() { codegen.setOpenAPI(openAPI); CodegenModel cm1 = codegen.fromModel("MapTest1", test1); Assert.assertEquals(cm1.getDataType(), "Map"); - Assert.assertEquals(cm1.getParent(), "HashMap"); + Assert.assertEquals(cm1.getParent(), "HashMap"); Assert.assertEquals(cm1.getClassname(), "MapTest1"); Schema test2 = openAPI.getComponents().getSchemas().get("MapTest2"); @@ -631,4 +631,147 @@ public void escapeName() { assertEquals("_int", codegen.toApiVarName("int")); assertEquals("pony", codegen.toApiVarName("pony")); } + + @Test + public void testAnyType() { + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/any_type.yaml"); + JavaClientCodegen codegen = new JavaClientCodegen(); + + Schema test1 = openAPI.getComponents().getSchemas().get("AnyValueModel"); + codegen.setOpenAPI(openAPI); + CodegenModel cm1 = codegen.fromModel("AnyValueModel", test1); + Assert.assertEquals(cm1.getClassname(), "AnyValueModel"); + + final CodegenProperty property1 = cm1.allVars.get(0); + Assert.assertEquals(property1.baseName, "any_value"); + Assert.assertEquals(property1.dataType, "oas_any_type_not_mapped"); + Assert.assertTrue(property1.hasMore); + Assert.assertFalse(property1.isPrimitiveType); + Assert.assertFalse(property1.isContainer); + Assert.assertFalse(property1.isFreeFormObject); + Assert.assertTrue(property1.isAnyType); + + final CodegenProperty property2 = cm1.allVars.get(1); + Assert.assertEquals(property2.baseName, "any_value_with_desc"); + Assert.assertEquals(property2.dataType, "oas_any_type_not_mapped"); + Assert.assertTrue(property2.hasMore); + Assert.assertFalse(property2.required); + Assert.assertFalse(property2.isPrimitiveType); + Assert.assertFalse(property2.isContainer); + Assert.assertFalse(property2.isFreeFormObject); + Assert.assertTrue(property2.isAnyType); + + final CodegenProperty property3 = cm1.allVars.get(2); + Assert.assertEquals(property3.baseName, "any_value_nullable"); + Assert.assertEquals(property3.dataType, "oas_any_type_not_mapped"); + Assert.assertFalse(property3.hasMore); + Assert.assertFalse(property3.required); + Assert.assertFalse(property3.isPrimitiveType); + Assert.assertFalse(property3.isContainer); + Assert.assertFalse(property3.isFreeFormObject); + Assert.assertTrue(property3.isAnyType); + + Schema test2 = openAPI.getComponents().getSchemas().get("AnyValueModelInline"); + codegen.setOpenAPI(openAPI); + CodegenModel cm2 = codegen.fromModel("AnyValueModelInline", test2); + Assert.assertEquals(cm2.getClassname(), "AnyValueModelInline"); + + final CodegenProperty cp1 = cm2.vars.get(0); + Assert.assertEquals(cp1.baseName, "any_value"); + Assert.assertEquals(cp1.dataType, "oas_any_type_not_mapped"); + Assert.assertTrue(cp1.hasMore); + Assert.assertFalse(cp1.required); + Assert.assertFalse(cp1.isPrimitiveType); + Assert.assertFalse(cp1.isContainer); + Assert.assertFalse(cp1.isFreeFormObject); + Assert.assertTrue(cp1.isAnyType); + + final CodegenProperty cp2 = cm2.vars.get(1); + Assert.assertEquals(cp2.baseName, "any_value_with_desc"); + Assert.assertEquals(cp2.dataType, "oas_any_type_not_mapped"); + Assert.assertTrue(cp2.hasMore); + Assert.assertFalse(cp2.required); + Assert.assertFalse(cp2.isPrimitiveType); + Assert.assertFalse(cp2.isContainer); + Assert.assertFalse(cp2.isFreeFormObject); + Assert.assertTrue(cp2.isAnyType); + + final CodegenProperty cp3 = cm2.vars.get(2); + Assert.assertEquals(cp3.baseName, "any_value_nullable"); + Assert.assertEquals(cp3.dataType, "oas_any_type_not_mapped"); + Assert.assertTrue(cp3.hasMore); + Assert.assertFalse(cp3.required); + Assert.assertFalse(cp3.isPrimitiveType); + Assert.assertFalse(cp3.isContainer); + Assert.assertFalse(cp3.isFreeFormObject); + Assert.assertTrue(cp3.isAnyType); + + // map + final CodegenProperty cp4 = cm2.vars.get(3); + Assert.assertEquals(cp4.baseName, "map_any_value"); + Assert.assertEquals(cp4.dataType, "Map"); + Assert.assertTrue(cp4.hasMore); + Assert.assertFalse(cp4.required); + Assert.assertFalse(cp4.isPrimitiveType); + Assert.assertTrue(cp4.isContainer); + Assert.assertTrue(cp4.isMapContainer); + Assert.assertTrue(cp4.isFreeFormObject); + Assert.assertFalse(cp4.isAnyType); + + final CodegenProperty cp5 = cm2.vars.get(4); + Assert.assertEquals(cp5.baseName, "map_any_value_with_desc"); + Assert.assertEquals(cp5.dataType, "Map"); + Assert.assertTrue(cp5.hasMore); + Assert.assertFalse(cp5.required); + Assert.assertFalse(cp5.isPrimitiveType); + Assert.assertTrue(cp5.isContainer); + Assert.assertTrue(cp5.isMapContainer); + Assert.assertTrue(cp5.isFreeFormObject); + Assert.assertFalse(cp5.isAnyType); + + final CodegenProperty cp6 = cm2.vars.get(5); + Assert.assertEquals(cp6.baseName, "map_any_value_nullable"); + Assert.assertEquals(cp6.dataType, "Map"); + Assert.assertTrue(cp6.hasMore); + Assert.assertFalse(cp6.required); + Assert.assertFalse(cp6.isPrimitiveType); + Assert.assertTrue(cp6.isContainer); + Assert.assertTrue(cp6.isMapContainer); + Assert.assertTrue(cp6.isFreeFormObject); + Assert.assertFalse(cp6.isAnyType); + + // array + final CodegenProperty cp7 = cm2.vars.get(6); + Assert.assertEquals(cp7.baseName, "array_any_value"); + Assert.assertEquals(cp7.dataType, "List"); + Assert.assertTrue(cp7.hasMore); + Assert.assertFalse(cp7.required); + Assert.assertFalse(cp7.isPrimitiveType); + Assert.assertTrue(cp7.isContainer); + Assert.assertTrue(cp7.isListContainer); + Assert.assertFalse(cp7.isFreeFormObject); + Assert.assertFalse(cp7.isAnyType); + + final CodegenProperty cp8 = cm2.vars.get(7); + Assert.assertEquals(cp8.baseName, "array_any_value_with_desc"); + Assert.assertEquals(cp8.dataType, "List"); + Assert.assertTrue(cp8.hasMore); + Assert.assertFalse(cp8.required); + Assert.assertFalse(cp8.isPrimitiveType); + Assert.assertTrue(cp8.isContainer); + Assert.assertTrue(cp8.isListContainer); + Assert.assertFalse(cp8.isFreeFormObject); + Assert.assertFalse(cp8.isAnyType); + + final CodegenProperty cp9 = cm2.vars.get(8); + Assert.assertEquals(cp9.baseName, "array_any_value_nullable"); + Assert.assertEquals(cp9.dataType, "List"); + Assert.assertFalse(cp9.hasMore); + Assert.assertFalse(cp9.required); + Assert.assertFalse(cp9.isPrimitiveType); + Assert.assertTrue(cp9.isContainer); + Assert.assertTrue(cp9.isListContainer); + Assert.assertFalse(cp9.isFreeFormObject); + Assert.assertFalse(cp9.isAnyType); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/any_type.yaml b/modules/openapi-generator/src/test/resources/3_0/any_type.yaml new file mode 100644 index 000000000000..870633cce56c --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/any_type.yaml @@ -0,0 +1,59 @@ +openapi: 3.0.1 +info: + title: ping test + version: '1.0' +servers: + - url: 'http://localhost:8000/' +paths: + /ping: + get: + operationId: pingGet + responses: + '201': + description: OK +components: + schemas: + AnyValue: {} + AnyValueWithDesc: + description: Can be any value - string, number, boolean, array or object. + AnyValueNullable: + nullable: true + description: Can be any value, including `null`. + AnyValueModel: + description: test any value + type: object + properties: + any_value: + $ref: '#/components/schemas/AnyValue' + any_value_with_desc: + $ref: '#/components/schemas/AnyValueWithDesc' + any_value_nullable: + $ref: '#/components/schemas/AnyValueNullable' + AnyValueModelInline: + description: test any value inline + type: object + properties: + any_value: {} + any_value_with_desc: + description: inline any value + any_value_nullable: + nullable: true + description: inline any value nullable + map_any_value: + additionalProperties: {} + map_any_value_with_desc: + additionalProperties: + description: inline any value + map_any_value_nullable: + additionalProperties: + nullable: true + description: inline any value nullable + array_any_value: + items: {} + array_any_value_with_desc: + items: + description: inline any value + array_any_value_nullable: + items: + nullable: true + description: inline any value nullable diff --git a/modules/openapi-generator/src/test/resources/3_0/issue796.yaml b/modules/openapi-generator/src/test/resources/3_0/issue796.yaml index 6ed6f3756e75..7d8d142059c5 100644 --- a/modules/openapi-generator/src/test/resources/3_0/issue796.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/issue796.yaml @@ -34,4 +34,13 @@ components: type: object description: This type example 4 additionalProperties: false - + MapObject: + properties: + map_test1: + $ref: '#/components/schemas/MapTest1' + map_test2: + $ref: '#/components/schemas/MapTest2' + map_test3: + $ref: '#/components/schemas/MapTest3' + other_obj: + $ref: '#/components/schemas/OtherObj' \ No newline at end of file diff --git a/samples/openapi3/client/petstore/go-experimental/go-petstore/docs/User.md b/samples/openapi3/client/petstore/go-experimental/go-petstore/docs/User.md index 4a78be2ab845..f40be495a5ab 100644 --- a/samples/openapi3/client/petstore/go-experimental/go-petstore/docs/User.md +++ b/samples/openapi3/client/petstore/go-experimental/go-petstore/docs/User.md @@ -14,8 +14,8 @@ Name | Type | Description | Notes **UserStatus** | Pointer to **int32** | User Status | [optional] **ArbitraryObject** | Pointer to [**map[string]interface{}**](.md) | test code generation for objects Value must be a map of strings to values. It cannot be the 'null' value. | [optional] **ArbitraryNullableObject** | Pointer to [**map[string]interface{}**](.md) | test code generation for nullable objects. Value must be a map of strings to values or the 'null' value. | [optional] -**ArbitraryTypeValue** | Pointer to **interface{}** | test code generation for any type Value can be any type - string, number, boolean, array or object. | [optional] -**ArbitraryNullableTypeValue** | Pointer to **interface{}** | test code generation for any type Value can be any type - string, number, boolean, array, object or the 'null' value. | [optional] +**ArbitraryTypeValue** | Pointer to [**interface{}**](.md) | test code generation for any type Value can be any type - string, number, boolean, array or object. | [optional] +**ArbitraryNullableTypeValue** | Pointer to [**interface{}**](.md) | test code generation for any type Value can be any type - string, number, boolean, array, object or the 'null' value. | [optional] ## Methods