Skip to content

Commit d1a8df0

Browse files
committed
[core] Add x-is-free-form vendor extension
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 068ad02 commit d1a8df0

File tree

4 files changed

+125
-7
lines changed

4 files changed

+125
-7
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 {
@@ -668,25 +670,23 @@ public static boolean isEmailSchema(Schema schema) {
668670
}
669671

670672
/**
671-
* Check to see if the schema is a model with at least one property.
673+
* Check to see if the schema is a model
672674
*
673675
* @param schema potentially containing a '$ref'
674676
* @return true if it's a model with at least one properties
675677
*/
676678
public static boolean isModel(Schema schema) {
677679
if (schema == null) {
678-
// TODO: Is this message necessary? A null schema is not a model, so the result is correct.
679-
once(LOGGER).error("Schema cannot be null in isModel check");
680680
return false;
681681
}
682682

683-
// has at least one property
684-
if (schema.getProperties() != null && !schema.getProperties().isEmpty()) {
683+
// has properties
684+
if (null != schema.getProperties()) {
685685
return true;
686686
}
687687

688-
// composed schema is a model
689-
return schema instanceof ComposedSchema;
688+
// composed schema is a model, consider very simple ObjectSchema a model
689+
return schema instanceof ComposedSchema || schema instanceof ObjectSchema;
690690
}
691691

692692
/**
@@ -741,6 +741,16 @@ public static boolean isFreeFormObject(OpenAPI openAPI, Schema schema) {
741741
// no properties
742742
if ((schema.getProperties() == null || schema.getProperties().isEmpty())) {
743743
Schema addlProps = getAdditionalProperties(openAPI, schema);
744+
745+
if (schema.getExtensions() != null && schema.getExtensions().containsKey(freeFormExplicit)) {
746+
// User has hard-coded vendor extension to handle free-form evaluation.
747+
boolean isFreeFormExplicit = Boolean.parseBoolean(String.valueOf(schema.getExtensions().get(freeFormExplicit)));
748+
if (!isFreeFormExplicit && addlProps != null && addlProps.getProperties() != null && !addlProps.getProperties().isEmpty()) {
749+
once(LOGGER).error(String.format(Locale.ROOT, "Potentially confusing usage of %s within model which defines additional properties", freeFormExplicit));
750+
}
751+
return isFreeFormExplicit;
752+
}
753+
744754
// additionalProperties not defined
745755
if (addlProps == null) {
746756
return true;

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,4 +927,32 @@ 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+
}
930958
}

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)