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 7072df85be8..fda66803235 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 class CodegenParameter implements IJsonSchemaValidationProperties { 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 class CodegenParameter implements IJsonSchemaValidationProperties { @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 class CodegenParameter implements IJsonSchemaValidationProperties { 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 class CodegenParameter implements IJsonSchemaValidationProperties { 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 1edad97316f..9e9d28958af 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 class CodegenResponse implements IJsonSchemaValidationProperties { 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 class CodegenResponse implements IJsonSchemaValidationProperties { 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 d4cc2541437..2480fa24fc9 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 class DefaultCodegen implements CodegenConfig { return legacyDiscriminatorBehavior; } - public void setLegacyDiscriminatorBehavior(boolean val){ + public void setLegacyDiscriminatorBehavior(boolean val) { this.legacyDiscriminatorBehavior = val; } @@ -1398,6 +1398,7 @@ public class DefaultCodegen implements CodegenConfig { 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 class DefaultCodegen implements CodegenConfig { /** * 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 @@ public class DefaultCodegen implements CodegenConfig { 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 class DefaultCodegen implements CodegenConfig { 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 class DefaultCodegen implements CodegenConfig { * 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 @@ public class DefaultCodegen implements CodegenConfig { 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 @@ public class DefaultCodegen implements CodegenConfig { 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 @@ public class DefaultCodegen implements CodegenConfig { 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 @@ public class DefaultCodegen implements CodegenConfig { * 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 @@ public class DefaultCodegen implements CodegenConfig { 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 @@ public class DefaultCodegen implements CodegenConfig { // 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 @@ public class DefaultCodegen implements CodegenConfig { // 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 @@ public class DefaultCodegen implements CodegenConfig { * 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 @@ public class DefaultCodegen implements CodegenConfig { 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 @@ public class DefaultCodegen implements CodegenConfig { 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 @@ public class DefaultCodegen implements CodegenConfig { } 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 @@ public class DefaultCodegen implements CodegenConfig { 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 @@ public class DefaultCodegen implements CodegenConfig { 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 @@ public class DefaultCodegen implements CodegenConfig { 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 @@ public class DefaultCodegen implements CodegenConfig { // 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 class DefaultCodegen implements CodegenConfig { } 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 class DefaultCodegen implements CodegenConfig { } 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 class DefaultCodegen implements CodegenConfig { 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 class DefaultCodegen implements CodegenConfig { } 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 class DefaultCodegen implements CodegenConfig { 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 @@ public class DefaultCodegen implements CodegenConfig { 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 class DefaultCodegen implements CodegenConfig { 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 class DefaultCodegen implements CodegenConfig { 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 class DefaultCodegen implements CodegenConfig { /** * 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 d4c0a8514e6..b018d22707a 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 abstract class AbstractGoCodegen extends DefaultCodegen implements Codege // 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 077676af99b..74796111afd 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 class PythonClientCodegen extends DefaultCodegen implements CodegenConfig 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 6dbefe36db4..7092f296171 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 class ModelUtils { * @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 class ModelUtils { * 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 class ModelUtils { * 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 class ModelUtils { * 3) additionalproperties is not defined, or additionalproperties: true, or additionalproperties: {}. * * Examples: - * + * * components: * schemas: * arbitraryObject: @@ -719,7 +725,7 @@ public class ModelUtils { * 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 class ModelUtils { 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 @@ public class ModelUtils { // 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 class ModelUtils { 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 class ModelUtils { if (refedWithoutDiscriminator.size() == 1) { if (hasAmbiguousParents) { LOGGER.warn("[deprecated] inheritance without use of 'discriminator.propertyName' is deprecated " + - "and will be removed in a future release. Generating model for composed schema name: {}. Title: {}", - composedSchema.getName(), composedSchema.getTitle()); + "and will be removed in a future release. Generating model for composed schema name: {}. Title: {}", + composedSchema.getName(), composedSchema.getTitle()); } return refedWithoutDiscriminator.get(0); } @@ -1269,12 +1280,10 @@ public class ModelUtils { Schema s = allSchemas.get(parentName); if (s != null) { return hasOrInheritsDiscriminator(s, allSchemas); - } - else { + } else { LOGGER.error("Failed to obtain schema from {}", parentName); } - } - else if (schema instanceof ComposedSchema) { + } else if (schema instanceof ComposedSchema) { final ComposedSchema composed = (ComposedSchema) schema; final List interfaces = getInterfaces(composed); for (Schema i : interfaces) { @@ -1376,7 +1385,7 @@ public class ModelUtils { 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 38b75b750ab..fa3f2b15248 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 class JavaClientCodegenTest { 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 class JavaClientCodegenTest { 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 00000000000..870633cce56 --- /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 6ed6f3756e7..7d8d142059c 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 4a78be2ab84..f40be495a5a 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