Skip to content

Commit a97feaf

Browse files
authored
[core] Add x-is-free-form vendor extension (#6849)
This adds an x-is-free-form vendor extension to allow users to skip our "free-form" logic which would previously prevent object schemas with no properties to be considered "free-form". The previous behavior was due in part to Swagger Parser not exposing `additionalProperties: false` to us (which should be similar behavior to this extension). A free-form object is considered a dynamic object with any number of properties/types. DefaultGenerator does not allow for generation of models considered free-form. However, a base type with no properties and no additional properties is allowed by OpenAPI Specification and is meaningful in many languages (e.g. "marker interfaces" or abstract closed types).
1 parent 54a6c79 commit a97feaf

File tree

4 files changed

+128
-9
lines changed

4 files changed

+128
-9
lines changed

modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ public class ModelUtils {
6565
// A vendor extension to track the value of the 'disallowAdditionalPropertiesIfNotPresent' CLI
6666
private static final String disallowAdditionalPropertiesIfNotPresent = "x-disallow-additional-properties-if-not-present";
6767

68+
private static final String freeFormExplicit = "x-is-free-form";
69+
6870
private static ObjectMapper JSON_MAPPER, YAML_MAPPER;
6971

7072
static {
@@ -672,25 +674,23 @@ public static boolean isEmailSchema(Schema schema) {
672674
}
673675

674676
/**
675-
* Check to see if the schema is a model with at least one property.
677+
* Check to see if the schema is a model
676678
*
677679
* @param schema potentially containing a '$ref'
678680
* @return true if it's a model with at least one properties
679681
*/
680682
public static boolean isModel(Schema schema) {
681683
if (schema == null) {
682-
// TODO: Is this message necessary? A null schema is not a model, so the result is correct.
683-
once(LOGGER).error("Schema cannot be null in isModel check");
684684
return false;
685685
}
686686

687-
// has at least one property
688-
if (schema.getProperties() != null && !schema.getProperties().isEmpty()) {
687+
// has properties
688+
if (null != schema.getProperties()) {
689689
return true;
690690
}
691691

692-
// composed schema is a model
693-
return schema instanceof ComposedSchema;
692+
// composed schema is a model, consider very simple ObjectSchema a model
693+
return schema instanceof ComposedSchema || schema instanceof ObjectSchema;
694694
}
695695

696696
/**
@@ -745,6 +745,16 @@ public static boolean isFreeFormObject(OpenAPI openAPI, Schema schema) {
745745
// no properties
746746
if ((schema.getProperties() == null || schema.getProperties().isEmpty())) {
747747
Schema addlProps = getAdditionalProperties(openAPI, schema);
748+
749+
if (schema.getExtensions() != null && schema.getExtensions().containsKey(freeFormExplicit)) {
750+
// User has hard-coded vendor extension to handle free-form evaluation.
751+
boolean isFreeFormExplicit = Boolean.parseBoolean(String.valueOf(schema.getExtensions().get(freeFormExplicit)));
752+
if (!isFreeFormExplicit && addlProps != null && addlProps.getProperties() != null && !addlProps.getProperties().isEmpty()) {
753+
once(LOGGER).error(String.format(Locale.ROOT, "Potentially confusing usage of %s within model which defines additional properties", freeFormExplicit));
754+
}
755+
return isFreeFormExplicit;
756+
}
757+
748758
// additionalProperties not defined
749759
if (addlProps == null) {
750760
return true;

modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,7 @@ public void testAuthorizationsHasMoreWhenFiltered() {
552552
assertEquals(getCodegenOperation.authMethods.size(), 2);
553553
assertTrue(getCodegenOperation.authMethods.get(0).hasMore);
554554
Assert.assertFalse(getCodegenOperation.authMethods.get(1).hasMore);
555-
}
555+
}
556556

557557
@Test
558558
public void testFreeFormObjects() {
@@ -881,7 +881,7 @@ public void testRestTemplateFormMultipart() throws IOException {
881881
//single file
882882
"multipartSingleWithHttpInfo(File file)",
883883
"formParams.add(\"file\", new FileSystemResource(file));"
884-
);
884+
);
885885
}
886886

887887
/**
@@ -927,6 +927,35 @@ public void testWebClientFormMultipart() throws IOException {
927927
);
928928
}
929929

930+
@Test
931+
public void testAllowModelWithNoProperties() throws Exception {
932+
File output = Files.createTempDirectory("test").toFile();
933+
934+
final CodegenConfigurator configurator = new CodegenConfigurator()
935+
.setGeneratorName("java")
936+
.setLibrary(JavaClientCodegen.OKHTTP_GSON)
937+
.setInputSpec("src/test/resources/2_0/emptyBaseModel.yaml")
938+
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));
939+
940+
final ClientOptInput clientOptInput = configurator.toClientOptInput();
941+
DefaultGenerator generator = new DefaultGenerator();
942+
List<File> files = generator.opts(clientOptInput).generate();
943+
944+
Assert.assertEquals(files.size(), 47);
945+
TestUtils.ensureContainsFile(files, output, "src/main/java/org/openapitools/client/model/RealCommand.java");
946+
TestUtils.ensureContainsFile(files, output, "src/main/java/org/openapitools/client/model/Command.java");
947+
948+
validateJavaSourceFiles(files);
949+
950+
TestUtils.assertFileContains(Paths.get(output + "/src/main/java/org/openapitools/client/model/RealCommand.java"),
951+
"class RealCommand extends Command");
952+
953+
TestUtils.assertFileContains(Paths.get(output + "/src/main/java/org/openapitools/client/model/Command.java"),
954+
"class Command");
955+
956+
output.deleteOnExit();
957+
}
958+
930959
/**
931960
* See https://github.com/OpenAPITools/openapi-generator/issues/6715
932961
*/

modules/openapi-generator/src/test/java/org/openapitools/codegen/utils/ModelUtilsTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,15 @@ public void testGlobalProducesConsumes() {
128128
Assert.assertEquals(unusedSchemas.size(), 0);
129129
}
130130

131+
@Test
132+
public void testIsModelAllowsEmptyBaseModel() {
133+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/2_0/emptyBaseModel.yaml");
134+
Schema commandSchema = ModelUtils.getSchema(openAPI, "Command");
135+
136+
Assert.assertTrue(ModelUtils.isModel(commandSchema));
137+
Assert.assertFalse(ModelUtils.isFreeFormObject(openAPI, commandSchema));
138+
}
139+
131140
@Test
132141
public void testReferencedSchema() {
133142
Schema otherObj = new ObjectSchema().addProperties("sprop", new StringSchema()).addProperties("iprop", new IntegerSchema());
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
swagger: "2.0"
2+
info:
3+
title: Test Command model generation
4+
description: Test Command model generation
5+
version: 1.0.0
6+
host: localhost:8080
7+
schemes:
8+
- https
9+
definitions:
10+
Command:
11+
title: Command
12+
description: The base object for all command objects.
13+
type: object
14+
# Explicitly avoid treating as a "free-form" or dynamic object, resulting in classical languages as a class with no properties.
15+
x-is-free-form: false
16+
RealCommand:
17+
title: RealCommand
18+
description: The real command.
19+
allOf:
20+
- $ref: '#/definitions/Command'
21+
ApiError:
22+
description: The base object for API errors.
23+
type: object
24+
required:
25+
- code
26+
- message
27+
properties:
28+
code:
29+
description: The error code. Usually, it is the HTTP error code.
30+
type: string
31+
readOnly: true
32+
message:
33+
description: The error message.
34+
type: string
35+
readOnly: true
36+
title: ApiError
37+
parameters:
38+
b_real_command:
39+
name: real_command
40+
in: body
41+
description: A payload for executing a real command.
42+
required: true
43+
schema:
44+
$ref: '#/definitions/RealCommand'
45+
paths:
46+
/execute:
47+
post:
48+
produces: []
49+
operationId: executeRealCommand
50+
parameters:
51+
- name: real_command
52+
in: body
53+
description: A payload for executing a real command.
54+
required: true
55+
schema:
56+
$ref: '#/definitions/RealCommand'
57+
responses:
58+
'204':
59+
description: Successful request. No content returned.
60+
'400':
61+
description: Bad request.
62+
schema:
63+
$ref: '#/definitions/ApiError'
64+
'404':
65+
description: Not found.
66+
schema:
67+
$ref: '#/definitions/ApiError'
68+
default:
69+
description: Unknown error.
70+
schema:
71+
$ref: '#/definitions/ApiError'

0 commit comments

Comments
 (0)