diff --git a/src/main/java/io/swagger/codegen/v3/generators/DefaultCodegenConfig.java b/src/main/java/io/swagger/codegen/v3/generators/DefaultCodegenConfig.java index 20f0f8d9e7..14e908007b 100644 --- a/src/main/java/io/swagger/codegen/v3/generators/DefaultCodegenConfig.java +++ b/src/main/java/io/swagger/codegen/v3/generators/DefaultCodegenConfig.java @@ -99,16 +99,8 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; -import static io.swagger.codegen.v3.CodegenConstants.HAS_ONLY_READ_ONLY_EXT_NAME; -import static io.swagger.codegen.v3.CodegenConstants.HAS_OPTIONAL_EXT_NAME; -import static io.swagger.codegen.v3.CodegenConstants.HAS_REQUIRED_EXT_NAME; -import static io.swagger.codegen.v3.CodegenConstants.IS_ARRAY_MODEL_EXT_NAME; -import static io.swagger.codegen.v3.CodegenConstants.IS_CONTAINER_EXT_NAME; -import static io.swagger.codegen.v3.CodegenConstants.IS_ENUM_EXT_NAME; -import static io.swagger.codegen.v3.generators.CodegenHelper.getDefaultIncludes; -import static io.swagger.codegen.v3.generators.CodegenHelper.getImportMappings; -import static io.swagger.codegen.v3.generators.CodegenHelper.getTypeMappings; -import static io.swagger.codegen.v3.generators.CodegenHelper.initalizeSpecialCharacterMapping; +import static io.swagger.codegen.v3.CodegenConstants.*; +import static io.swagger.codegen.v3.generators.CodegenHelper.*; import static io.swagger.codegen.v3.generators.handlebars.ExtensionHelper.getBooleanValue; public abstract class DefaultCodegenConfig implements CodegenConfig { @@ -1344,9 +1336,6 @@ public CodegenModel fromModel(String name, Schema schema, Map al codegenModel.getVendorExtensions().put(CodegenConstants.IS_ALIAS_EXT_NAME, typeAliases.containsKey(name)); codegenModel.discriminator = schema.getDiscriminator(); - if (codegenModel.discriminator != null && codegenModel.discriminator.getPropertyName() != null) { - codegenModel.discriminator.setPropertyName(toVarName(codegenModel.discriminator.getPropertyName())); - } if (schema.getXml() != null) { codegenModel.xmlPrefix = schema.getXml().getPrefix(); @@ -1397,6 +1386,25 @@ else if (schema instanceof ComposedSchema) { } } } + if (codegenModel.discriminator != null && codegenModel.discriminator.getPropertyName() != null) { + codegenModel.discriminator.setPropertyName(toVarName(codegenModel.discriminator.getPropertyName())); + Map classnameKeys = new HashMap<>(); + + if (composed.getOneOf()!=null) { + composed.getOneOf().forEach( s -> { + codegenModel.discriminator.getMapping().keySet().stream().filter( key -> codegenModel.discriminator.getMapping().get(key).equals(s.get$ref())) + .forEach(key -> { + String mappingValue = codegenModel.discriminator.getMapping().get(key); + if (classnameKeys.containsKey(codegenModel.classname)) { + throw new IllegalArgumentException("Duplicate shema name in discriminator mapping"); + } + classnameKeys.put(toModelName(mappingValue.replace("#/components/schemas/", "")),key); + }); + }); + codegenModel.discriminator.getMapping().putAll(classnameKeys); + } + } + } else { allProperties = null; allRequired = null; @@ -4415,7 +4423,7 @@ protected void setParameterJson(CodegenParameter codegenParameter, Schema parame codegenParameter.isJson = true; } } - + protected boolean isFileTypeSchema(Schema schema) { final Schema fileTypeSchema; if (StringUtils.isNotBlank(schema.get$ref())) { diff --git a/src/main/java/io/swagger/codegen/v3/generators/SchemaHandler.java b/src/main/java/io/swagger/codegen/v3/generators/SchemaHandler.java index 09f0e13cd9..315f8151cd 100644 --- a/src/main/java/io/swagger/codegen/v3/generators/SchemaHandler.java +++ b/src/main/java/io/swagger/codegen/v3/generators/SchemaHandler.java @@ -199,9 +199,7 @@ protected void addInterfaces(List schemas, CodegenModel codegenModel, Ma codegenModel.addSubType(model); } - if (codegenModel.getVendorExtensions() == null || codegenModel.getVendorExtensions().containsKey("x-discriminator-type")) { - continue; - } + if (codegenModel.getDiscriminator() != null && StringUtils.isNotBlank(codegenModel.getDiscriminator().getPropertyName())) { Optional optionalProperty = model.vars.stream() .filter(codegenProperty -> codegenProperty.baseName.equals(codegenModel.getDiscriminator().getPropertyName())).findFirst(); diff --git a/src/main/resources/handlebars/Java/interface.mustache b/src/main/resources/handlebars/Java/interface.mustache index 0d08bbe545..d3d7202e24 100644 --- a/src/main/resources/handlebars/Java/interface.mustache +++ b/src/main/resources/handlebars/Java/interface.mustache @@ -9,10 +9,10 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, - property = "type") + property = "{{discriminator.propertyName}}") @JsonSubTypes({ {{#subTypes}} - @JsonSubTypes.Type(value = {{classname}}.class, name = "{{classname}}"){{^@last}},{{/@last}} + @JsonSubTypes.Type(value = {{classname}}.class, name = "{{subtypeName}}"){{^@last}},{{/@last}} {{/subTypes}} }) {{/jackson}} diff --git a/src/main/resources/handlebars/Java/typeInfoAnnotation.mustache b/src/main/resources/handlebars/Java/typeInfoAnnotation.mustache index eeda9eb1cf..a6a3a6a71a 100644 --- a/src/main/resources/handlebars/Java/typeInfoAnnotation.mustache +++ b/src/main/resources/handlebars/Java/typeInfoAnnotation.mustache @@ -1,14 +1,8 @@ {{#jackson}} @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "{{discriminator.propertyName}}", visible = true ) @JsonSubTypes({ -{{#if discriminator.mapping}} -{{#each discriminator.mapping}} - @JsonSubTypes.Type(value = {{this}}.class, name = "{{@key}}"), -{{/each}} -{{else}} {{#children}} - @JsonSubTypes.Type(value = {{classname}}.class, name = "{{name}}"), + @JsonSubTypes.Type(value = {{classname}}.class, name = "{{^vendorExtensions.x-discriminator-value}}{{subtypeName}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{{vendorExtensions.x-discriminator-value}}}{{/vendorExtensions.x-discriminator-value}}"), {{/children}} -{{/if}} }) -{{/jackson}} +{{/jackson}} \ No newline at end of file diff --git a/src/test/java/io/swagger/codegen/v3/generators/GeneratorRunner.java b/src/test/java/io/swagger/codegen/v3/generators/GeneratorRunner.java index 73b15b0d63..0385fbb4fe 100644 --- a/src/test/java/io/swagger/codegen/v3/generators/GeneratorRunner.java +++ b/src/test/java/io/swagger/codegen/v3/generators/GeneratorRunner.java @@ -13,6 +13,8 @@ import java.io.InputStream; import java.nio.file.Files; import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; /** * @@ -28,7 +30,8 @@ public static List runGenerator( boolean v2Spec, boolean yaml, boolean flattenInlineComposedSchema, - String outFolder + String outFolder, + Consumer optionsCustomizer ) throws Exception { String path = outFolder; @@ -36,6 +39,13 @@ public static List runGenerator( path = getTmpFolder().getAbsolutePath(); } GenerationRequest request = new GenerationRequest(); + + Options option = new Options() + .flattenInlineComposedSchema(flattenInlineComposedSchema) + .outputDir(path); + + optionsCustomizer.accept(option); + request .codegenVersion(codegenVersion) // use V2 to target Swagger/OpenAPI 2.x Codegen version .type(GenerationRequest.Type.CLIENT) @@ -44,9 +54,7 @@ public static List runGenerator( yaml, // YAML file, use false for JSON v2Spec)) // OpenAPI 3.x - use true for Swagger/OpenAPI 2.x definitions .options( - new Options() - .flattenInlineComposedSchema(flattenInlineComposedSchema) - .outputDir(path) + option ); List files = new GeneratorService().generationRequest(request).generate(); diff --git a/src/test/java/io/swagger/codegen/v3/generators/java/GeneratorResultTestJava.java b/src/test/java/io/swagger/codegen/v3/generators/java/GeneratorResultTestJava.java index 58509df237..41cd2587e6 100644 --- a/src/test/java/io/swagger/codegen/v3/generators/java/GeneratorResultTestJava.java +++ b/src/test/java/io/swagger/codegen/v3/generators/java/GeneratorResultTestJava.java @@ -1,12 +1,18 @@ package io.swagger.codegen.v3.generators.java; +import com.fasterxml.jackson.annotation.JsonSubTypes; import io.swagger.codegen.v3.generators.GeneratorRunner; import io.swagger.codegen.v3.service.GenerationRequest; +import org.apache.commons.io.FileUtils; import org.testng.Assert; import org.testng.annotations.Test; import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class GeneratorResultTestJava { @@ -29,11 +35,109 @@ public void testJavaGenerator_OneOf() throws Exception { v2Spec, yaml, flattenInlineComposedSchema, - outFolder); + outFolder, options -> {}); Assert.assertFalse(files.isEmpty()); for (File f: files) { // TODO test stuff } } + + @Test + public void interfaceWithCustomDiscriminator() throws Exception { + + String name = "java"; + String specPath = "3_0_0/sample_interface_with_discriminator.json"; + GenerationRequest.CodegenVersion codegenVersion = GenerationRequest.CodegenVersion.V3; + boolean v2Spec = false; // 3.0 spec + boolean yaml = false; + boolean flattenInlineComposedSchema = true; + String outFolder = null; // temporary folder + + File tmpFolder = GeneratorRunner.getTmpFolder(); + Assert.assertNotNull(tmpFolder); + + List files = GeneratorRunner.runGenerator( + name, + specPath, + codegenVersion, + v2Spec, + yaml, + flattenInlineComposedSchema, + tmpFolder.getAbsolutePath(), + options -> options.setLibrary("resttemplate")); + + + File interfaceFile = files.stream().filter(f -> f.getName().equals("Item.java")).findAny().orElseThrow(() -> new RuntimeException("No interface generated")); + + String interfaceContent = new String(Files.readAllBytes(Paths.get(interfaceFile.toURI()))); + + Pattern typeInfoPattern = Pattern.compile("(.*)(@JsonTypeInfo\\()(.*)(}\\))(.*)", Pattern.DOTALL); + + Matcher matcher = typeInfoPattern.matcher(interfaceContent); + + Assert.assertTrue(matcher.matches(), + "No JsonTypeInfo generated into the interface file"); + + String generatedTypeInfoLines = matcher.group(2)+matcher.group(3)+matcher.group(4); + + Assert.assertEquals( generatedTypeInfoLines, "@JsonTypeInfo(" + System.lineSeparator() + + " use = JsonTypeInfo.Id.NAME," + System.lineSeparator() + + " include = JsonTypeInfo.As.PROPERTY," + System.lineSeparator() + + " property = \"aCustomProperty\")" + System.lineSeparator() + + "@JsonSubTypes({" + System.lineSeparator() + + " @JsonSubTypes.Type(value = ClassA.class, name = \"typeA\")," + System.lineSeparator() + + " @JsonSubTypes.Type(value = ClassB.class, name = \"typeB\")," + System.lineSeparator() + + " @JsonSubTypes.Type(value = ClassC.class, name = \"typeC\")" + System.lineSeparator() + + "})", "Wrong json subtypes generated"); + + FileUtils.deleteDirectory(new File(tmpFolder.getAbsolutePath())); + } + + @Test + public void javaCustomDiscriminator() throws Exception { + + String name = "java"; + String specPath = "3_0_0/javaDiscriminatorExample.yaml"; + GenerationRequest.CodegenVersion codegenVersion = GenerationRequest.CodegenVersion.V3; + boolean v2Spec = false; // 3.0 spec + boolean yaml = true; + boolean flattenInlineComposedSchema = true; + String outFolder = null; // temporary folder + + File tmpFolder = GeneratorRunner.getTmpFolder(); + Assert.assertNotNull(tmpFolder); + + List files = GeneratorRunner.runGenerator( + name, + specPath, + codegenVersion, + v2Spec, + yaml, + flattenInlineComposedSchema, + tmpFolder.getAbsolutePath(), + options -> options.setLibrary("resttemplate")); + + + File interfaceFile = files.stream().filter(f -> f.getName().equals("ResultForSubTypeDTO.java")).findAny().orElseThrow(() -> new RuntimeException("No class generated")); + + String interfaceContent = new String(Files.readAllBytes(Paths.get(interfaceFile.toURI()))); + + Pattern typeInfoPattern = Pattern.compile("(.*)(@JsonTypeInfo\\()(.*)(}\\))(.*)", Pattern.DOTALL); + + Matcher matcher = typeInfoPattern.matcher(interfaceContent); + + Assert.assertTrue(matcher.matches(), + "No JsonTypeInfo generated into the interface file"); + + String generatedTypeInfoLines = matcher.group(2)+matcher.group(3)+matcher.group(4); + + Assert.assertEquals( generatedTypeInfoLines, "@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = \"type\", visible = true )" + System.lineSeparator() + + "@JsonSubTypes({" + System.lineSeparator() + + " @JsonSubTypes.Type(value = SubTypeBResultDTO.class, name = \"WeirdlyNamedSubTypeB\")," + System.lineSeparator() + + " @JsonSubTypes.Type(value = SubTypeAResultDTO.class, name = \"SubTypeA\")," + System.lineSeparator() + + "})"); + + FileUtils.deleteDirectory(new File(tmpFolder.getAbsolutePath())); + } } diff --git a/src/test/java/io/swagger/codegen/v3/generators/java/JavaPolymorphicAnnotationCodegenTest.java b/src/test/java/io/swagger/codegen/v3/generators/java/JavaPolymorphicAnnotationCodegenTest.java index d3ad04763c..c749d39662 100644 --- a/src/test/java/io/swagger/codegen/v3/generators/java/JavaPolymorphicAnnotationCodegenTest.java +++ b/src/test/java/io/swagger/codegen/v3/generators/java/JavaPolymorphicAnnotationCodegenTest.java @@ -31,10 +31,10 @@ public void testParameterOrders() throws Exception { final String content = FileUtils.readFileToString(petControllerFile); Assert.assertTrue(content.contains( - "@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = \"type\", visible = true )\n" + - "@JsonSubTypes({\n" + - " @JsonSubTypes.Type(value = Error.class, name = \"Error\"),\n" + - " @JsonSubTypes.Type(value = Success.class, name = \"Success\"),\n" + + "@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = \"type\", visible = true )" + System.lineSeparator() + + "@JsonSubTypes({" + System.lineSeparator() + + " @JsonSubTypes.Type(value = Error.class, name = \"Error\")," + System.lineSeparator() + + " @JsonSubTypes.Type(value = Success.class, name = \"Success\")," + System.lineSeparator() + "})")); this.folder.delete(); diff --git a/src/test/resources/3_0_0/javaDiscriminatorExample.yaml b/src/test/resources/3_0_0/javaDiscriminatorExample.yaml new file mode 100644 index 0000000000..39a67601b9 --- /dev/null +++ b/src/test/resources/3_0_0/javaDiscriminatorExample.yaml @@ -0,0 +1,52 @@ +openapi: '3.0.3' +info: + title: 'Discriminator Problem API' + version: '1.0.0' + +components: + schemas: + SomeTypeDTO: + type: string + enum: + - SubTypeA + - WeirdlyNamedSubTypeB + + ResultForSubTypeDTO: + type: object + properties: + type: + type: string + oneOf: + - $ref: '#/components/schemas/SubTypeAResultDTO' + - $ref: '#/components/schemas/SubTypeBResultDTO' + required: + - type + discriminator: + propertyName: type + mapping: + SubTypeA: '#/components/schemas/SubTypeAResultDTO' + WeirdlyNamedSubTypeB: '#/components/schemas/SubTypeBResultDTO' + + SubTypeAResultDTO: + allOf: + - $ref: '#/components/schemas/ResultForSubTypeDTO' + type: object + properties: + some_attribute: + type: string + + SubTypeBResultDTO: + allOf: + - $ref: '#/components/schemas/ResultForSubTypeDTO' + type: object + properties: + another_attribute: + type: string + +paths: + /repro: + get: + operationId: 'getRepo' + responses: + 204: + description: OK \ No newline at end of file diff --git a/src/test/resources/3_0_0/sample_interface_with_discriminator.json b/src/test/resources/3_0_0/sample_interface_with_discriminator.json new file mode 100644 index 0000000000..0ebc9df6cf --- /dev/null +++ b/src/test/resources/3_0_0/sample_interface_with_discriminator.json @@ -0,0 +1,118 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Title", + "description": "Title", + "version": "1.0.0" + }, + "paths": { + "/sampleObjectResponse": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "Returns requested data", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/sampleResponse" + } + } + } + } + + } + } + + } + }, + "components": { + "schemas": { + "sampleResponse": { + "type": "array", + "items": { + "$ref": "#/components/schemas/item" + } + }, + "item": { + "discriminator": { + "propertyName": "aCustomProperty", + "mapping": { + "typeA": "#/components/schemas/classA", + "typeB": "#/components/schemas/classB", + "typeC": "#/components/schemas/classC" + } + }, + "oneOf": [ + { + "$ref": "#/components/schemas/classA" + }, + { + "$ref": "#/components/schemas/classB" + }, + { + "$ref": "#/components/schemas/classC" + } + ] + }, + "classA": { + "type": "object", + "properties": { + "aaa": { + "type": "string" + } + }, + "allOf": [ + { + "$ref": "#/components/schemas/baseClass" + } + ] + }, + "classB": { + "type": "object", + "properties": { + "bbb": { + "type": "string" + } + }, + "allOf": [ + { + "$ref": "#/components/schemas/baseClass" + } + ] + }, + "classC": { + "type": "object", + "properties": { + "ccc": { + "type": "string" + } + }, + "allOf": [ + { + "$ref": "#/components/schemas/baseClass" + } + ] + }, + "baseClass": { + + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + } + } + }, + "tags": [ + { + "name": "Sample", + "description": "Sample" + } + ] +}