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 4c3e9fe0118..1b5f74bed47 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 @@ -65,6 +65,8 @@ public class ModelUtils { // A vendor extension to track the value of the 'disallowAdditionalPropertiesIfNotPresent' CLI private static final String disallowAdditionalPropertiesIfNotPresent = "x-disallow-additional-properties-if-not-present"; + private static final String freeFormExplicit = "x-is-free-form"; + private static ObjectMapper JSON_MAPPER, YAML_MAPPER; static { @@ -672,25 +674,23 @@ public class ModelUtils { } /** - * Check to see if the schema is a model with at least one property. + * Check to see if the schema is a model * * @param schema potentially containing a '$ref' * @return true if it's a model with at least one properties */ public static boolean isModel(Schema schema) { if (schema == null) { - // TODO: Is this message necessary? A null schema is not a model, so the result is correct. - once(LOGGER).error("Schema cannot be null in isModel check"); return false; } - // has at least one property - if (schema.getProperties() != null && !schema.getProperties().isEmpty()) { + // has properties + if (null != schema.getProperties()) { return true; } - // composed schema is a model - return schema instanceof ComposedSchema; + // composed schema is a model, consider very simple ObjectSchema a model + return schema instanceof ComposedSchema || schema instanceof ObjectSchema; } /** @@ -745,6 +745,16 @@ public class ModelUtils { // no properties if ((schema.getProperties() == null || schema.getProperties().isEmpty())) { Schema addlProps = getAdditionalProperties(openAPI, schema); + + if (schema.getExtensions() != null && schema.getExtensions().containsKey(freeFormExplicit)) { + // User has hard-coded vendor extension to handle free-form evaluation. + boolean isFreeFormExplicit = Boolean.parseBoolean(String.valueOf(schema.getExtensions().get(freeFormExplicit))); + if (!isFreeFormExplicit && addlProps != null && addlProps.getProperties() != null && !addlProps.getProperties().isEmpty()) { + once(LOGGER).error(String.format(Locale.ROOT, "Potentially confusing usage of %s within model which defines additional properties", freeFormExplicit)); + } + return isFreeFormExplicit; + } + // additionalProperties not defined if (addlProps == null) { return true; 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 f5ea089d1ed..f4ad38c2ec3 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 @@ -552,7 +552,7 @@ public class JavaClientCodegenTest { assertEquals(getCodegenOperation.authMethods.size(), 2); assertTrue(getCodegenOperation.authMethods.get(0).hasMore); Assert.assertFalse(getCodegenOperation.authMethods.get(1).hasMore); - } + } @Test public void testFreeFormObjects() { @@ -881,7 +881,7 @@ public class JavaClientCodegenTest { //single file "multipartSingleWithHttpInfo(File file)", "formParams.add(\"file\", new FileSystemResource(file));" - ); + ); } /** @@ -927,6 +927,35 @@ public class JavaClientCodegenTest { ); } + @Test + public void testAllowModelWithNoProperties() throws Exception { + File output = Files.createTempDirectory("test").toFile(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("java") + .setLibrary(JavaClientCodegen.OKHTTP_GSON) + .setInputSpec("src/test/resources/2_0/emptyBaseModel.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(clientOptInput).generate(); + + Assert.assertEquals(files.size(), 47); + TestUtils.ensureContainsFile(files, output, "src/main/java/org/openapitools/client/model/RealCommand.java"); + TestUtils.ensureContainsFile(files, output, "src/main/java/org/openapitools/client/model/Command.java"); + + validateJavaSourceFiles(files); + + TestUtils.assertFileContains(Paths.get(output + "/src/main/java/org/openapitools/client/model/RealCommand.java"), + "class RealCommand extends Command"); + + TestUtils.assertFileContains(Paths.get(output + "/src/main/java/org/openapitools/client/model/Command.java"), + "class Command"); + + output.deleteOnExit(); + } + /** * See https://github.com/OpenAPITools/openapi-generator/issues/6715 */ diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/utils/ModelUtilsTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/utils/ModelUtilsTest.java index 221f13a7004..e0e67ceb4fd 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/utils/ModelUtilsTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/utils/ModelUtilsTest.java @@ -128,6 +128,15 @@ public class ModelUtilsTest { Assert.assertEquals(unusedSchemas.size(), 0); } + @Test + public void testIsModelAllowsEmptyBaseModel() { + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/2_0/emptyBaseModel.yaml"); + Schema commandSchema = ModelUtils.getSchema(openAPI, "Command"); + + Assert.assertTrue(ModelUtils.isModel(commandSchema)); + Assert.assertFalse(ModelUtils.isFreeFormObject(openAPI, commandSchema)); + } + @Test public void testReferencedSchema() { Schema otherObj = new ObjectSchema().addProperties("sprop", new StringSchema()).addProperties("iprop", new IntegerSchema()); diff --git a/modules/openapi-generator/src/test/resources/2_0/emptyBaseModel.yaml b/modules/openapi-generator/src/test/resources/2_0/emptyBaseModel.yaml new file mode 100644 index 00000000000..b5bfde2f6ee --- /dev/null +++ b/modules/openapi-generator/src/test/resources/2_0/emptyBaseModel.yaml @@ -0,0 +1,71 @@ +swagger: "2.0" +info: + title: Test Command model generation + description: Test Command model generation + version: 1.0.0 +host: localhost:8080 +schemes: + - https +definitions: + Command: + title: Command + description: The base object for all command objects. + type: object + # Explicitly avoid treating as a "free-form" or dynamic object, resulting in classical languages as a class with no properties. + x-is-free-form: false + RealCommand: + title: RealCommand + description: The real command. + allOf: + - $ref: '#/definitions/Command' + ApiError: + description: The base object for API errors. + type: object + required: + - code + - message + properties: + code: + description: The error code. Usually, it is the HTTP error code. + type: string + readOnly: true + message: + description: The error message. + type: string + readOnly: true + title: ApiError +parameters: + b_real_command: + name: real_command + in: body + description: A payload for executing a real command. + required: true + schema: + $ref: '#/definitions/RealCommand' +paths: + /execute: + post: + produces: [] + operationId: executeRealCommand + parameters: + - name: real_command + in: body + description: A payload for executing a real command. + required: true + schema: + $ref: '#/definitions/RealCommand' + responses: + '204': + description: Successful request. No content returned. + '400': + description: Bad request. + schema: + $ref: '#/definitions/ApiError' + '404': + description: Not found. + schema: + $ref: '#/definitions/ApiError' + default: + description: Unknown error. + schema: + $ref: '#/definitions/ApiError' \ No newline at end of file