Skip to content

Commit 2c67841

Browse files
authored
Add option to set container's default to empty container (OpenAPITools#21269)
* add default to empty container option * test map default to empty container * update java generators to respect default value * various fixes * fix tests * update doc
1 parent dcd89bf commit 2c67841

File tree

7 files changed

+354
-10
lines changed

7 files changed

+354
-10
lines changed

bin/configs/java-okhttp-gson.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ parameterNameMappings:
1010
_type: underscoreType
1111
type_: typeWithUnderscore
1212
additionalProperties:
13+
defaultToEmptyContainer: "array?|array|map?"
1314
artifactId: petstore-okhttp-gson
1415
hideGenerationTimestamp: true
1516
useOneOfDiscriminatorLookup: true

docs/customization.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,32 @@ or
401401
--import-mappings Pet=my.models.MyPet --import-mappings Order=my.models.MyOrder
402402
```
403403
404+
## Default Values
405+
406+
To customize the default values for containers, one can leverage the option `defaultToEmptyContainer` to customize what to initalize for array/set/map by respecting the default values in the spec
407+
408+
Set optional array and map default value to an empty container
409+
```
410+
java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java -i modules/openapi-generator/src/test/resources/3_0/petstore.yaml -o /tmp/output --additional-properties defaultToEmptyContainer="array?|map?"
411+
```
412+
413+
Set nullable array (required) default value to an empty container
414+
```
415+
java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java -i modules/openapi-generator/src/test/resources/3_0/petstore.yaml -o /tmp/output --additional-properties defaultToEmptyContainer="?array"
416+
```
417+
418+
Set nullable array (optional) default value to an empty container
419+
```
420+
java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java -i modules/openapi-generator/src/test/resources/3_0/petstore.yaml -o /tmp/output --additional-properties defaultToEmptyContainer="?array?"
421+
```
422+
423+
To simply enable this option to respect default values in the specification (basically null if not specified):
424+
```
425+
java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java -i modules/openapi-generator/src/test/resources/3_0/petstore.yaml -o /tmp/output --additional-properties defaultToEmptyContainer=""
426+
```
427+
428+
Note: not all generators support this generator's option (e.g. --additional-properties defaultToEmptyContainer="?array" in CLI) so please test to confirm. Java generators are the first to implement this feature. We welcome PRs to support this option in other generators. Related PR: https://github.com/OpenAPITools/openapi-generator/pull/21269
429+
404430
## Name Mapping
405431
406432
One can map the property name using `nameMappings` option and parameter name using `parameterNameMappings` option to something else. Consider the following schema:

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

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,12 @@ apiTemplateFiles are for API outputs only (controllers/handlers).
334334
// Whether to automatically hardcode params that are considered Constants by OpenAPI Spec
335335
@Setter protected boolean autosetConstants = false;
336336

337+
@Setter @Getter boolean arrayDefaultToEmpty, arrayNullableDefaultToEmpty, arrayOptionalNullableDefaultToEmpty, arrayOptionalDefaultToEmpty;
338+
@Setter @Getter boolean mapDefaultToEmpty, mapNullableDefaultToEmpty, mapOptionalNullableDefaultToEmpty, mapOptionalDefaultToEmpty;
339+
@Setter @Getter protected boolean defaultToEmptyContainer;
340+
final String DEFAULT_TO_EMPTY_CONTAINER = "defaultToEmptyContainer";
341+
final List EMPTY_LIST = new ArrayList();
342+
337343
@Override
338344
public boolean getAddSuffixToDuplicateOperationNicknames() {
339345
return addSuffixToDuplicateOperationNicknames;
@@ -392,8 +398,12 @@ public void processOpts() {
392398
convertPropertyToBooleanAndWriteBack(CodegenConstants.DISALLOW_ADDITIONAL_PROPERTIES_IF_NOT_PRESENT, this::setDisallowAdditionalPropertiesIfNotPresent);
393399
convertPropertyToBooleanAndWriteBack(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, this::setEnumUnknownDefaultCase);
394400
convertPropertyToBooleanAndWriteBack(CodegenConstants.AUTOSET_CONSTANTS, this::setAutosetConstants);
395-
}
396401

402+
if (additionalProperties.containsKey(DEFAULT_TO_EMPTY_CONTAINER) && additionalProperties.get(DEFAULT_TO_EMPTY_CONTAINER) instanceof String) {
403+
parseDefaultToEmptyContainer((String) additionalProperties.get(DEFAULT_TO_EMPTY_CONTAINER));
404+
defaultToEmptyContainer = true;
405+
}
406+
}
397407

398408
/***
399409
* Preset map builder with commonly used Mustache lambdas.
@@ -4226,6 +4236,11 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo
42264236
}
42274237
}
42284238

4239+
// override defaultValue if it's not set and defaultToEmptyContainer is set
4240+
if (p.getDefault() == null && defaultToEmptyContainer) {
4241+
updateDefaultToEmptyContainer(property, p);
4242+
}
4243+
42294244
// set the default value
42304245
property.defaultValue = toDefaultValue(property, p);
42314246
property.defaultValueWithParam = toDefaultValueWithParam(name, p);
@@ -4235,6 +4250,99 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo
42354250
return property;
42364251
}
42374252

4253+
/**
4254+
* update container's default to empty container according rules provided by the user.
4255+
*
4256+
* @param cp codegen property
4257+
* @param p schema
4258+
*/
4259+
void updateDefaultToEmptyContainer(CodegenProperty cp, Schema p) {
4260+
if (cp.isArray) {
4261+
if (!cp.required) { // optional
4262+
if (cp.isNullable && arrayOptionalNullableDefaultToEmpty) { // nullable
4263+
p.setDefault(EMPTY_LIST);
4264+
} else if (!cp.isNullable && arrayOptionalDefaultToEmpty) { // non-nullable
4265+
p.setDefault(EMPTY_LIST);
4266+
}
4267+
} else { // required
4268+
if (cp.isNullable && arrayNullableDefaultToEmpty) { // nullable
4269+
p.setDefault(EMPTY_LIST);
4270+
} else if (!cp.isNullable && arrayDefaultToEmpty) { // non-nullable
4271+
p.setDefault(EMPTY_LIST);
4272+
}
4273+
}
4274+
} else if (cp.isMap) {
4275+
if (!cp.required) { // optional
4276+
if (cp.isNullable && mapOptionalNullableDefaultToEmpty) { // nullable
4277+
p.setDefault(EMPTY_LIST);
4278+
} else if (!cp.isNullable && mapOptionalDefaultToEmpty) { // non-nullable
4279+
p.setDefault(EMPTY_LIST);
4280+
}
4281+
} else { // required
4282+
if (cp.isNullable && mapNullableDefaultToEmpty) { // nullable
4283+
p.setDefault(EMPTY_LIST);
4284+
} else if (!cp.isNullable && mapOptionalDefaultToEmpty) { // non-nullable
4285+
p.setDefault(EMPTY_LIST);
4286+
}
4287+
}
4288+
}
4289+
}
4290+
4291+
/**
4292+
* Parse the rules for defaulting to the empty container.
4293+
*
4294+
* @param input a set of rules separated by `|`
4295+
*/
4296+
void parseDefaultToEmptyContainer(String input) {
4297+
String[] inputs = ((String) input).split("[|]");
4298+
String containerType;
4299+
for (String rule: inputs) {
4300+
if (StringUtils.isEmpty(rule)) {
4301+
LOGGER.error("updateDefaultToEmptyContainer: Skipped empty input in `{}`.", input);
4302+
continue;
4303+
}
4304+
4305+
if (rule.startsWith("?") && rule.endsWith("?")) { // nullable optional
4306+
containerType = rule.substring(1, rule.length() - 1);
4307+
if ("array".equalsIgnoreCase(containerType)) {
4308+
arrayOptionalNullableDefaultToEmpty = true;
4309+
} else if ("map".equalsIgnoreCase(containerType)) {
4310+
mapOptionalNullableDefaultToEmpty = true;
4311+
} else {
4312+
LOGGER.error("Skipped invalid container type `{}` in `{}`.", containerType, input);
4313+
}
4314+
} else if (rule.startsWith("?")) { // nullable (required)
4315+
containerType = rule.substring(1, rule.length());
4316+
if ("array".equalsIgnoreCase(containerType)) {
4317+
arrayNullableDefaultToEmpty = true;
4318+
} else if ("map".equalsIgnoreCase(containerType)) {
4319+
mapNullableDefaultToEmpty = true;
4320+
} else {
4321+
LOGGER.error("Skipped invalid container type `{}` in `{}`.", containerType, input);
4322+
}
4323+
} else if (rule.endsWith("?")) { // optional
4324+
containerType = rule.substring(0, rule.length()-1);
4325+
if ("array".equalsIgnoreCase(containerType)) {
4326+
arrayOptionalDefaultToEmpty = true;
4327+
} else if ("map".equalsIgnoreCase(containerType)) {
4328+
mapOptionalDefaultToEmpty = true;
4329+
} else {
4330+
LOGGER.error("Skipped invalid container type `{}` in the rule `{}`.", containerType, input);
4331+
}
4332+
} else { // required
4333+
containerType = rule;
4334+
if ("array".equalsIgnoreCase(containerType)) {
4335+
arrayDefaultToEmpty = true;
4336+
} else if ("map".equalsIgnoreCase(containerType)) {
4337+
mapDefaultToEmpty = true;
4338+
} else {
4339+
LOGGER.error("Skipped invalid container type `{}` in the rule `{}`.", containerType, input);
4340+
}
4341+
}
4342+
4343+
}
4344+
}
4345+
42384346
/**
42394347
* Update property for array(list) container
42404348
*

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,7 +1242,7 @@ public String toArrayDefaultValue(CodegenProperty cp, Schema schema) {
12421242
if (schema.getDefault() instanceof ArrayNode) { // array of default values
12431243
ArrayNode _default = (ArrayNode) schema.getDefault();
12441244
if (_default.isEmpty()) { // e.g. default: []
1245-
return getDefaultCollectionType(schema);
1245+
return getDefaultCollectionType(schema, "");
12461246
}
12471247

12481248
List<String> final_values = _values;
@@ -1255,6 +1255,11 @@ public String toArrayDefaultValue(CodegenProperty cp, Schema schema) {
12551255
_default.forEach((element) -> {
12561256
final_values.add(String.valueOf(element));
12571257
});
1258+
1259+
if (_default != null && _default.isEmpty() && defaultToEmptyContainer) {
1260+
// e.g. [] with the option defaultToEmptyContainer enabled
1261+
return getDefaultCollectionType(schema, "");
1262+
}
12581263
} else { // single value
12591264
_values = java.util.Collections.singletonList(String.valueOf(schema.getDefault()));
12601265
}
@@ -1308,7 +1313,10 @@ public String toArrayDefaultValue(CodegenProperty cp, Schema schema) {
13081313
public String toDefaultValue(CodegenProperty cp, Schema schema) {
13091314
schema = ModelUtils.getReferencedSchema(this.openAPI, schema);
13101315
if (ModelUtils.isArraySchema(schema)) {
1311-
if (schema.getDefault() == null) {
1316+
if (defaultToEmptyContainer) {
1317+
// if default to empty container option is set, respect the default values provided in the spec
1318+
return toArrayDefaultValue(cp, schema);
1319+
} else if (schema.getDefault() == null) {
13121320
// nullable or containerDefaultToNull set to true
13131321
if (cp.isNullable || containerDefaultToNull) {
13141322
return null;
@@ -1325,6 +1333,16 @@ public String toDefaultValue(CodegenProperty cp, Schema schema) {
13251333
return null;
13261334
}
13271335

1336+
if (defaultToEmptyContainer) {
1337+
// respect the default values provided in the spec when the option is enabled
1338+
if (schema.getDefault() != null) {
1339+
return String.format(Locale.ROOT, "new %s<>()",
1340+
instantiationTypes().getOrDefault("map", "HashMap"));
1341+
} else {
1342+
return null;
1343+
}
1344+
}
1345+
13281346
// nullable or containerDefaultToNull set to true
13291347
if (cp.isNullable || containerDefaultToNull) {
13301348
return null;
@@ -1487,12 +1505,31 @@ private String getDefaultCollectionType(Schema schema) {
14871505

14881506
private String getDefaultCollectionType(Schema schema, String defaultValues) {
14891507
String arrayFormat = "new %s<>(Arrays.asList(%s))";
1508+
1509+
if (defaultToEmptyContainer) {
1510+
// respect the default value in the spec
1511+
if (defaultValues == null) { // default value not provided
1512+
return null;
1513+
} else if (defaultValues.isEmpty()) { // e.g. [] to indicates empty container
1514+
arrayFormat = "new %s<>()";
1515+
return getDefaultCollectionType(arrayFormat, defaultValues, ModelUtils.isSet(schema));
1516+
} else { // default value not empty
1517+
return getDefaultCollectionType(arrayFormat, defaultValues, ModelUtils.isSet(schema));
1518+
}
1519+
}
1520+
14901521
if (defaultValues == null || defaultValues.isEmpty()) {
1522+
// default to empty container even though default value is null
1523+
// to respect default values provided in the spec, set the option `defaultToEmptyContainer` properly
14911524
defaultValues = "";
14921525
arrayFormat = "new %s<>()";
14931526
}
14941527

1495-
if (ModelUtils.isSet(schema)) {
1528+
return getDefaultCollectionType(arrayFormat, defaultValues, ModelUtils.isSet(schema));
1529+
}
1530+
1531+
private String getDefaultCollectionType(String arrayFormat, String defaultValues, boolean isSet) {
1532+
if (isSet) {
14961533
return String.format(Locale.ROOT, arrayFormat,
14971534
instantiationTypes().getOrDefault("set", "LinkedHashSet"), defaultValues);
14981535
}

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5501,4 +5501,35 @@ public void testEnumFieldShouldBeFinal_issue21018() throws IOException {
55015501
JavaFileAssert.assertThat(files.get("SomeObject.java"))
55025502
.fileContains("private final String value");
55035503
}
5504+
5505+
@Test
5506+
public void testCollectionTypesWithDefaults_issue_collection() throws IOException {
5507+
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
5508+
output.deleteOnExit();
5509+
5510+
OpenAPI openAPI = new OpenAPIParser()
5511+
.readLocation("src/test/resources/3_0/java/issue_collection.yaml", null, new ParseOptions()).getOpenAPI();
5512+
SpringCodegen codegen = new SpringCodegen();
5513+
codegen.setLibrary(SPRING_CLOUD_LIBRARY);
5514+
codegen.setOutputDir(output.getAbsolutePath());
5515+
codegen.additionalProperties().put(CodegenConstants.MODEL_PACKAGE, "xyz.model");
5516+
codegen.additionalProperties().put(CodegenConstants.API_NAME_SUFFIX, "Controller");
5517+
codegen.additionalProperties().put(CodegenConstants.API_PACKAGE, "xyz.controller");
5518+
codegen.additionalProperties().put(CodegenConstants.MODEL_NAME_SUFFIX, "Dto");
5519+
codegen.additionalProperties().put("defaultToEmptyContainer", "array");
5520+
5521+
ClientOptInput input = new ClientOptInput()
5522+
.openAPI(openAPI)
5523+
.config(codegen);
5524+
5525+
DefaultGenerator generator = new DefaultGenerator();
5526+
Map<String, File> files = generator.opts(input).generate().stream()
5527+
.collect(Collectors.toMap(File::getName, Function.identity()));
5528+
5529+
JavaFileAssert.assertThat(files.get("PetDto.java"))
5530+
.fileContains("private @Nullable List<@Valid TagDto> tags;")
5531+
.fileContains("private List<@Valid TagDto> tagsRequiredList = new ArrayList<>();")
5532+
.fileContains("private @Nullable List<String> stringList;")
5533+
.fileContains("private List<String> stringRequiredList = new ArrayList<>();");
5534+
}
55045535
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
openapi: 3.0.0
2+
servers:
3+
- url: 'http://petstore.swagger.io/v2'
4+
info:
5+
description: >-
6+
This is a sample server Petstore server. For this sample, you can use the api key
7+
`special-key` to test the authorization filters.
8+
version: 1.0.0
9+
title: OpenAPI Petstore
10+
license:
11+
name: Apache-2.0
12+
url: 'https://www.apache.org/licenses/LICENSE-2.0.html'
13+
paths:
14+
'/pet/{petId}':
15+
get:
16+
tags:
17+
- pet
18+
summary: Find pet by ID
19+
description: Returns a single pet
20+
operationId: getPetById
21+
parameters:
22+
- name: petId
23+
in: path
24+
description: ID of pet to return
25+
required: true
26+
schema:
27+
type: integer
28+
format: int64
29+
responses:
30+
'200':
31+
description: successful operation
32+
content:
33+
application/json:
34+
schema:
35+
$ref: '#/components/schemas/Pet'
36+
'400':
37+
description: Invalid ID supplied
38+
'404':
39+
description: Pet not found
40+
components:
41+
schemas:
42+
Tag:
43+
title: Pet Tag
44+
description: A tag for a pet
45+
type: object
46+
properties:
47+
id:
48+
type: integer
49+
format: int64
50+
name:
51+
type: string
52+
Pet:
53+
title: a Pet
54+
description: A pet for sale in the pet store
55+
type: object
56+
required:
57+
- tagsRequiredList
58+
- stringRequiredList
59+
properties:
60+
tags:
61+
type: array
62+
items:
63+
$ref: '#/components/schemas/Tag'
64+
tagsRequiredList:
65+
type: array
66+
items:
67+
$ref: '#/components/schemas/Tag'
68+
stringList:
69+
type: array
70+
items:
71+
type: string
72+
stringRequiredList:
73+
type: array
74+
items:
75+
type: string

0 commit comments

Comments
 (0)