Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Codegen] Convert "any type" to oneOf model #6051

Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,26 @@ public class CodegenModel implements IJsonSchemaValidationProperties {
public Set<String> allMandatory = new TreeSet<String>(); // with parent's required properties

public Set<String> imports = new TreeSet<String>();
public boolean hasVars, emptyVars, hasMoreModels, hasEnums, isEnum, isNullable, hasRequired, hasOptional, isArrayModel, hasChildren, isMapModel, isDeprecated;
public boolean hasVars, emptyVars, hasMoreModels, hasEnums, isEnum;
/**
* Indicates the OAS schema specifies "nullable: true".
*/
public boolean isNullable;
/**
* Indicates the type has at least one required property.
*/
public boolean hasRequired;
/**
* Indicates the type has at least one optional property.
*/
public boolean hasOptional;
public boolean isArrayModel;
public boolean hasChildren;
public boolean isMapModel;
/**
* Indicates the OAS schema specifies "deprecated: true".
*/
public boolean isDeprecated;
public boolean hasOnlyReadOnly = true; // true if all properties are read-only
public ExternalDocumentation externalDocumentation;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1774,6 +1774,10 @@ public String toDefaultValueWithParam(String name, Schema schema) {
**/
@SuppressWarnings("static-method")
public String getSchemaType(Schema schema) {
if (schema == null) {
// This is indicative of a bug in codegen.
throw new RuntimeException("getSchemaType schema argument is null");
}
if (schema instanceof ComposedSchema) { // composed schema
ComposedSchema cs = (ComposedSchema) schema;
// Get the interfaces, i.e. the set of elements under 'allOf', 'anyOf' or 'oneOf'.
Expand Down Expand Up @@ -1886,7 +1890,9 @@ public String toOneOfName(List<String> names, ComposedSchema composedSchema) {
*/
private String getSingleSchemaType(Schema schema) {
Schema unaliasSchema = ModelUtils.unaliasSchema(this.openAPI, schema, importMapping);

if (unaliasSchema == null) {
throw new RuntimeException("Schema '" + schema.getName() + "' is invalid");
}
if (StringUtils.isNotBlank(unaliasSchema.get$ref())) { // reference to another definition/schema
// get the schema/model name from $ref
String schemaName = ModelUtils.getSimpleRef(unaliasSchema.get$ref());
Expand Down Expand Up @@ -2010,15 +2016,15 @@ public String getTypeDeclaration(String name) {
}

/**
* Output the type declaration of the property
* Output the language-specific type declaration of the property.
*
* @param schema property schema
* @return a string presentation of the property type
*/
public String getTypeDeclaration(Schema schema) {
if (schema == null) {
LOGGER.warn("Null schema found. Default type to `NULL_SCHMEA_ERR`");
return "NULL_SCHMEA_ERR";
LOGGER.warn("Null schema found. Default type to `NULL_SCHEMA_ERR`");
return "NULL_SCHEMA_ERR";
}

String oasType = getSchemaType(schema);
Expand Down Expand Up @@ -2100,6 +2106,70 @@ public String toModelName(final String name) {
return camelize(modelNamePrefix + "_" + name + "_" + modelNameSuffix);
}

/**
* Returns a composed model that encapsulates the JSON schema "any type".
* Its value can be any of null, integer, boolean, number, string, array or map.
*/
protected Schema getAnyTypeSchema(String name, Schema schema) {
if (!ModelUtils.isAnyTypeSchema(schema)) {
throw new RuntimeException("Schema '" + name + "' is not 'any type'");
}
ComposedSchema cs = (ComposedSchema) new ComposedSchema()
.addAnyOfItem(new ObjectSchema().type("null"))
.addAnyOfItem(new BooleanSchema())
.addAnyOfItem(new StringSchema()
.minLength(schema.getMinLength())
.maxLength(schema.getMaxLength())
.pattern(schema.getPattern())
)
.addAnyOfItem(new IntegerSchema()
.minimum(schema.getMinimum())
.maximum(schema.getMaximum())
.exclusiveMinimum(schema.getExclusiveMinimum())
.exclusiveMaximum(schema.getExclusiveMaximum())
.multipleOf(schema.getMultipleOf())
)
.addAnyOfItem(new NumberSchema()
.minimum(schema.getMinimum())
.maximum(schema.getMaximum())
.exclusiveMinimum(schema.getExclusiveMinimum())
.exclusiveMaximum(schema.getExclusiveMaximum())
.multipleOf(schema.getMultipleOf())
)
.name(name);

// The map keys must be strings and the values can be anything.
cs.addAnyOfItem(new MapSchema()
.additionalProperties(true)
.minProperties(schema.getMinProperties())
.maxProperties(schema.getMaxProperties())
.required(schema.getRequired())
);
// The array items can be anything.
cs.addAnyOfItem(new ArraySchema()
.minItems(schema.getMinItems())
.maxItems(schema.getMaxItems())
.uniqueItems(schema.getUniqueItems())
);
if (schema != null) {
cs.setTitle(schema.getTitle());
cs.setDescription(schema.getDescription());
Copy link
Contributor Author

@sebastien-rosset sebastien-rosset Apr 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially I thought I should create a single "AnyType" schema, but in reality there may be corner cases where constraints other than "type" have been specified:
title
pattern
required
enum
minimum
maximum
exclusiveMinimum
exclusiveMaximum
multipleOf
minLength
maxLength
minItems
maxItems
uniqueItems
minProperties
maxProperties

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But aren't those constraints specific to types? If you had minItems it would only apply to dict and array, right? My take is if they have any of those constraints then they should be fully explicit and list all types. We are just trying to cover this one super general yoy said it could be anything case. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That way that constraint question is the headache of specific generators. I agree with you about adding this model/schema once and then using it multiple places if the writers used it multiple places.

Copy link
Contributor Author

@sebastien-rosset sebastien-rosset Apr 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But aren't those constraints specific to types? If you had minItems it would only apply to dict and array, right? My take is if they have any of those constraints then they should be fully explicit and list all types. We are just trying to cover this one super general yoy said it could be anything case. What do you think?

I am planning to handle these edge cases later. Right now just trying to make the simple case work, i.e. no OAS attribute whatsoever is defined in the OAS schema, it's really any type. Even this simple case is not so simple.

Also, the good news is most of these constraints are specific to a type. The only constraints that apply to all types are type, enum and const: https://tools.ietf.org/html/draft-handrews-json-schema-validation-02

}
return cs;
}

// Returns a model that encapsulates the JSON schema "any type". Its value
// can be any of null, integer, boolean, number, string, array or map.
// "Any type" is a schema that does not have the "type" attribute
// specified in the OpenAPI schema. That means the value can be any valid
// payload, i.e. the null value, boolean, string, integer, number,
// array or map.
// Numerical payloads may match more than one type, for example "2" may
// match integer and number. Hence the use of 'anyOf'.
public CodegenModel getAnyTypeModel(String name, Schema schema) {
return fromModel(name, getAnyTypeSchema(name, schema));
}

/**
* Convert OAS Model object to Codegen Model object
*
Expand All @@ -2121,6 +2191,11 @@ public CodegenModel fromModel(String name, Schema schema) {
return null;
}

if (ModelUtils.isAnyTypeSchema(schema)) {
// "Any type" means the payload can be any type, e.g. integer, number, object, array...
return getAnyTypeModel(name, schema);
}
Copy link
Contributor

@spacether spacether Apr 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code only covers the case when the user include AnyType as a model schema.
Below I see that you you also handle the Model property case, but that code does not create a schema/model that the property can refer to.

What if we have AnyTypeSchema in:

  • a Model property
  • a Model
  • Inline in a response definition?

For example if AnyType is only in a model property AND/or an inline response definition there is no Model/Schema that it should be $ref connected to. In my opinion, we should make this model so it can be handled by all generators.

How about instead in the InlineModelResolver

  • pre-processing the the openapi document to check for the locations that need to be fixed
  • then insert qty 1 anyTypeModel into the openapi document
AnyType:
  anyOf:
  - type: "null"
  - type: integer
  - type: array ...
  • link all references that require it to that $ref
    Then we don't need any specific logic like you are adding in python-experimental.


CodegenModel m = CodegenModelFactory.newInstance(CodegenModelType.MODEL);

if (reservedWords.contains(name)) {
Expand Down Expand Up @@ -2155,7 +2230,6 @@ public CodegenModel fromModel(String name, Schema schema) {
m.xmlNamespace = schema.getXml().getNamespace();
m.xmlName = schema.getXml().getName();
}

if (ModelUtils.isArraySchema(schema)) {
m.isArrayModel = true;
m.arrayModelType = fromProperty(name, schema).complexType;
Expand Down Expand Up @@ -2801,7 +2875,7 @@ public String getterAndSetterCapitalize(String name) {
* Convert OAS Property object to Codegen Property object
*
* @param name name of the property
* @param p OAS property object
* @param p OAS property schema
* @return Codegen Property object
*/
public CodegenProperty fromProperty(String name, Schema p) {
Expand All @@ -2814,6 +2888,9 @@ public CodegenProperty fromProperty(String name, Schema p) {
// unalias schema
p = ModelUtils.unaliasSchema(this.openAPI, p, importMapping);

if (ModelUtils.isAnyTypeSchema(p)) {
p = getAnyTypeSchema(name, p);
}
CodegenProperty property = CodegenModelFactory.newInstance(CodegenModelType.PROPERTY);

ModelUtils.syncValidationProperties(p, property);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,10 +327,23 @@ public String getTypeDeclaration(Schema p) {
if (ModelUtils.isArraySchema(p)) {
ArraySchema ap = (ArraySchema) p;
Schema inner = ap.getItems();
return "[]" + getTypeDeclaration(ModelUtils.unaliasSchema(this.openAPI, inner));
// Per JSON schema specification, the "items" attribute in an Array schema
// is optional. When "items" is not specified, the elements of the array
// may be anything at all.
if (inner != null) {
inner = ModelUtils.unaliasSchema(this.openAPI, inner);
}
String typDecl;
if (inner != null) {
typDecl = getTypeDeclaration(inner);
} else {
typDecl = "interface{}";
}
return "[]" + typDecl;
} else if (ModelUtils.isMapSchema(p)) {
Schema inner = ModelUtils.getAdditionalProperties(p);
return getSchemaType(p) + "[string]" + getTypeDeclaration(ModelUtils.unaliasSchema(this.openAPI, inner));
inner = ModelUtils.unaliasSchema(this.openAPI, inner);
return getSchemaType(p) + "[string]" + getTypeDeclaration(inner);
}
//return super.getTypeDeclaration(p);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -910,7 +910,14 @@ public String getTypeString(Schema p, String prefix, String suffix) {
} else if (ModelUtils.isArraySchema(p)) {
ArraySchema ap = (ArraySchema) p;
Schema inner = ap.getItems();
return prefix + "[" + getTypeString(inner, "", "") + "]" + fullSuffix;
if (inner == null) {
// Per JSON schema specification, the array "items" attribute is optional.
// When "items" is not specified, the elements of the array
// may be anything at all.
return prefix + "[bool, date, datetime, dict, float, int, list, str, none_type]" + fullSuffix;
Copy link
Contributor Author

@sebastien-rosset sebastien-rosset Apr 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@spacether , can you help review this? Is this the right way to fix the issue?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this will only happen if we are in an array, right?
Don't we instead need to cover the ModelUtils.isAnyTypeSchema(schema) case?
How about higher up at line 907 use:

Schema anyType = new Schema();
if (ModelUtils.isAnyTypeSchema(schema)):
  return prefix + "bool, date, datetime, dict, float, int, list, str, none_type" + fullSufix;
} else if ((ModelUtils.isMapSchema(p) || "object".equals(p.getType())) && 
  ...
} else if (ModelUtils.isArraySchema(p)) 
  ArraySchema ap = (ArraySchema) p;
  Schema inner = ap.getItems();
  if (inner == null) {
    inner = anyType;
  }
  return prefix + "[" + getTypeString(inner, "", "") + "]" + fullSuffix;
...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need the if (ModelUtils.isAnyTypeSchema(schema)): in case we travel through an object(dict) to get there. In that case, our return would look like:
{str: (bool, date, datetime, dict, float, int, list, str, none_type)}

} else {
return prefix + "[" + getTypeString(inner, "", "") + "]" + fullSuffix;
}
}
if (ModelUtils.isFileSchema(p)) {
return prefix + "file_type" + fullSuffix;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ public static Object[][] primeNumbers() {
@Test(dataProvider = "modelNames", description = "avoid inner class")
public void modelNameTest(String name, String expectedName) {
OpenAPI openAPI = TestUtils.createOpenAPI();
final Schema model = new Schema();
final Schema model = new ObjectSchema();
final DefaultCodegen codegen = new DartClientCodegen();
codegen.setOpenAPI(openAPI);
final CodegenModel cm = codegen.fromModel(name, model);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io.swagger.v3.oas.models.media.IntegerSchema;
import io.swagger.v3.oas.models.media.MapSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.StringSchema;
import org.openapitools.codegen.ClientOptInput;
import org.openapitools.codegen.CodegenConstants;
Expand Down Expand Up @@ -375,7 +376,7 @@ public static Object[][] primeNumbers() {
@Test(dataProvider = "modelNames", description = "avoid inner class")
public void modelNameTest(String name, String expectedName) {
OpenAPI openAPI = TestUtils.createOpenAPI();
final Schema model = new Schema();
final Schema model = new ObjectSchema();
final DefaultCodegen codegen = new DartDioClientCodegen();
codegen.setOpenAPI(openAPI);
final CodegenModel cm = codegen.fromModel(name, model);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ public static Object[][] primeNumbers() {

@Test(dataProvider = "modelNames", description = "avoid inner class")
public void modelNameTest(String name, String expectedName) {
final Schema model = new Schema();
final Schema model = new ObjectSchema();
final DefaultCodegen codegen = new GoClientCodegen();
OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema(name, model);
codegen.setOpenAPI(openAPI);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,21 @@ public void complexPropertiesTest() {
Assert.assertFalse(property.isContainer);
}

@Test(description = "test 'any type' schema")
public void anyTypeSchemaTest() {
// Create a schema without any constraint.
final Schema schema = new Schema()
.description("a sample model");
final DefaultCodegen codegen = new JavaClientCodegen();
OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema("sample", schema);
codegen.setOpenAPI(openAPI);
final CodegenModel cm = codegen.fromModel("sample", schema);
//
Assert.assertEquals(cm.name, "sample");
Assert.assertEquals(cm.classname, "Sample");
Assert.assertEquals(cm.description, "a sample model");
}

@Test(description = "convert a model with complex list property")
public void complexListPropertyTest() {
final Schema schema = new Schema()
Expand Down Expand Up @@ -713,7 +728,7 @@ public static Object[][] primeNumbers() {

@Test(dataProvider = "modelNames", description = "avoid inner class")
public void modelNameTest(String name, String expectedName) {
final Schema schema = new Schema();
final Schema schema = new ObjectSchema();
final DefaultCodegen codegen = new JavaClientCodegen();
OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema(name, schema);
codegen.setOpenAPI(openAPI);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import org.openapitools.codegen.*;
Expand Down Expand Up @@ -55,7 +56,7 @@ static Object[][] reservedWords() {
@Test(dataProvider = "reservedWords")
public void testReservedWordsAsModels(String reservedWord) {
final DefaultCodegen codegen = new KotlinClientCodegen();
final Schema schema = new Schema();
final Schema schema = new ObjectSchema();
final String escaped = "`" + reservedWord + "`";
final String titleCased = StringUtils.camelize(reservedWord, false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ public static Object[][] primeNumbers() {

@Test(dataProvider = "modelNames", description = "avoid inner class")
public void modelNameTest(String name, String expectedName) {
final Schema model = new Schema();
final Schema model = new ObjectSchema();
final DefaultCodegen codegen = new PhpClientCodegen();
OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema(name, model);
codegen.setOpenAPI(openAPI);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ public void codeGenerationJava8Test() throws Exception {

@Test(description = "strip model name")
public void stripModelNameTest() throws Exception {
final Schema model = new Schema()
final Schema model = new ObjectSchema()
.description("a map model");
final DefaultCodegen codegen = new ScalaAkkaClientCodegen();
OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema("sample", model);
Expand Down