Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bin/configs/java-okhttp-gson.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ parameterNameMappings:
_type: underscoreType
type_: typeWithUnderscore
additionalProperties:
defaultToEmptyContainer: "array?|array|map?"
artifactId: petstore-okhttp-gson
hideGenerationTimestamp: true
useOneOfDiscriminatorLookup: true
Expand Down
26 changes: 26 additions & 0 deletions docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,32 @@ or
--import-mappings Pet=my.models.MyPet --import-mappings Order=my.models.MyOrder
```

## Default Values

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

Set optional array and map default value to an empty container
```
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?"
```

Set nullable array (required) default value to an empty container
```
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"
```

Set nullable array (optional) default value to an empty container
```
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?"
```

To simply enable this option to respect default values in the specification (basically null if not specified):
```
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=""
```

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

## Name Mapping

One can map the property name using `nameMappings` option and parameter name using `parameterNameMappings` option to something else. Consider the following schema:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,12 @@ apiTemplateFiles are for API outputs only (controllers/handlers).
// Whether to automatically hardcode params that are considered Constants by OpenAPI Spec
@Setter protected boolean autosetConstants = false;

@Setter @Getter boolean arrayDefaultToEmpty, arrayNullableDefaultToEmpty, arrayOptionalNullableDefaultToEmpty, arrayOptionalDefaultToEmpty;
@Setter @Getter boolean mapDefaultToEmpty, mapNullableDefaultToEmpty, mapOptionalNullableDefaultToEmpty, mapOptionalDefaultToEmpty;
@Setter @Getter protected boolean defaultToEmptyContainer;
final String DEFAULT_TO_EMPTY_CONTAINER = "defaultToEmptyContainer";
final List EMPTY_LIST = new ArrayList();

@Override
public boolean getAddSuffixToDuplicateOperationNicknames() {
return addSuffixToDuplicateOperationNicknames;
Expand Down Expand Up @@ -392,8 +398,12 @@ public void processOpts() {
convertPropertyToBooleanAndWriteBack(CodegenConstants.DISALLOW_ADDITIONAL_PROPERTIES_IF_NOT_PRESENT, this::setDisallowAdditionalPropertiesIfNotPresent);
convertPropertyToBooleanAndWriteBack(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, this::setEnumUnknownDefaultCase);
convertPropertyToBooleanAndWriteBack(CodegenConstants.AUTOSET_CONSTANTS, this::setAutosetConstants);
}

if (additionalProperties.containsKey(DEFAULT_TO_EMPTY_CONTAINER) && additionalProperties.get(DEFAULT_TO_EMPTY_CONTAINER) instanceof String) {
parseDefaultToEmptyContainer((String) additionalProperties.get(DEFAULT_TO_EMPTY_CONTAINER));
defaultToEmptyContainer = true;
}
}

/***
* Preset map builder with commonly used Mustache lambdas.
Expand Down Expand Up @@ -4226,6 +4236,11 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo
}
}

// override defaultValue if it's not set and defaultToEmptyContainer is set
if (p.getDefault() == null && defaultToEmptyContainer) {
updateDefaultToEmptyContainer(property, p);
}

// set the default value
property.defaultValue = toDefaultValue(property, p);
property.defaultValueWithParam = toDefaultValueWithParam(name, p);
Expand All @@ -4235,6 +4250,99 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo
return property;
}

/**
* update container's default to empty container according rules provided by the user.
*
* @param cp codegen property
* @param p schema
*/
void updateDefaultToEmptyContainer(CodegenProperty cp, Schema p) {
if (cp.isArray) {
if (!cp.required) { // optional
if (cp.isNullable && arrayOptionalNullableDefaultToEmpty) { // nullable
p.setDefault(EMPTY_LIST);
} else if (!cp.isNullable && arrayOptionalDefaultToEmpty) { // non-nullable
p.setDefault(EMPTY_LIST);
}
} else { // required
if (cp.isNullable && arrayNullableDefaultToEmpty) { // nullable
p.setDefault(EMPTY_LIST);
} else if (!cp.isNullable && arrayDefaultToEmpty) { // non-nullable
p.setDefault(EMPTY_LIST);
}
}
} else if (cp.isMap) {
if (!cp.required) { // optional
if (cp.isNullable && mapOptionalNullableDefaultToEmpty) { // nullable
p.setDefault(EMPTY_LIST);
} else if (!cp.isNullable && mapOptionalDefaultToEmpty) { // non-nullable
p.setDefault(EMPTY_LIST);
}
} else { // required
if (cp.isNullable && mapNullableDefaultToEmpty) { // nullable
p.setDefault(EMPTY_LIST);
} else if (!cp.isNullable && mapOptionalDefaultToEmpty) { // non-nullable
p.setDefault(EMPTY_LIST);
}
}
}
}

/**
* Parse the rules for defaulting to the empty container.
*
* @param input a set of rules separated by `|`
*/
void parseDefaultToEmptyContainer(String input) {
String[] inputs = ((String) input).split("[|]");
String containerType;
for (String rule: inputs) {
if (StringUtils.isEmpty(rule)) {
LOGGER.error("updateDefaultToEmptyContainer: Skipped empty input in `{}`.", input);
continue;
}

if (rule.startsWith("?") && rule.endsWith("?")) { // nullable optional
containerType = rule.substring(1, rule.length() - 1);
if ("array".equalsIgnoreCase(containerType)) {
arrayOptionalNullableDefaultToEmpty = true;
} else if ("map".equalsIgnoreCase(containerType)) {
mapOptionalNullableDefaultToEmpty = true;
} else {
LOGGER.error("Skipped invalid container type `{}` in `{}`.", containerType, input);
}
} else if (rule.startsWith("?")) { // nullable (required)
containerType = rule.substring(1, rule.length());
if ("array".equalsIgnoreCase(containerType)) {
arrayNullableDefaultToEmpty = true;
} else if ("map".equalsIgnoreCase(containerType)) {
mapNullableDefaultToEmpty = true;
} else {
LOGGER.error("Skipped invalid container type `{}` in `{}`.", containerType, input);
}
} else if (rule.endsWith("?")) { // optional
containerType = rule.substring(0, rule.length()-1);
if ("array".equalsIgnoreCase(containerType)) {
arrayOptionalDefaultToEmpty = true;
} else if ("map".equalsIgnoreCase(containerType)) {
mapOptionalDefaultToEmpty = true;
} else {
LOGGER.error("Skipped invalid container type `{}` in the rule `{}`.", containerType, input);
}
} else { // required
containerType = rule;
if ("array".equalsIgnoreCase(containerType)) {
arrayDefaultToEmpty = true;
} else if ("map".equalsIgnoreCase(containerType)) {
mapDefaultToEmpty = true;
} else {
LOGGER.error("Skipped invalid container type `{}` in the rule `{}`.", containerType, input);
}
}

}
}

/**
* Update property for array(list) container
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1240,7 +1240,7 @@ public String toArrayDefaultValue(CodegenProperty cp, Schema schema) {
if (schema.getDefault() instanceof ArrayNode) { // array of default values
ArrayNode _default = (ArrayNode) schema.getDefault();
if (_default.isEmpty()) { // e.g. default: []
return getDefaultCollectionType(schema);
return getDefaultCollectionType(schema, "");
}

List<String> final_values = _values;
Expand All @@ -1253,6 +1253,11 @@ public String toArrayDefaultValue(CodegenProperty cp, Schema schema) {
_default.forEach((element) -> {
final_values.add(String.valueOf(element));
});

if (_default != null && _default.isEmpty() && defaultToEmptyContainer) {
// e.g. [] with the option defaultToEmptyContainer enabled
return getDefaultCollectionType(schema, "");
}
} else { // single value
_values = java.util.Collections.singletonList(String.valueOf(schema.getDefault()));
}
Expand Down Expand Up @@ -1306,7 +1311,10 @@ public String toArrayDefaultValue(CodegenProperty cp, Schema schema) {
public String toDefaultValue(CodegenProperty cp, Schema schema) {
schema = ModelUtils.getReferencedSchema(this.openAPI, schema);
if (ModelUtils.isArraySchema(schema)) {
if (schema.getDefault() == null) {
if (defaultToEmptyContainer) {
// if default to empty container option is set, respect the default values provided in the spec
return toArrayDefaultValue(cp, schema);
} else if (schema.getDefault() == null) {
// nullable or containerDefaultToNull set to true
if (cp.isNullable || containerDefaultToNull) {
return null;
Expand All @@ -1323,6 +1331,16 @@ public String toDefaultValue(CodegenProperty cp, Schema schema) {
return null;
}

if (defaultToEmptyContainer) {
// respect the default values provided in the spec when the option is enabled
if (schema.getDefault() != null) {
return String.format(Locale.ROOT, "new %s<>()",
instantiationTypes().getOrDefault("map", "HashMap"));
} else {
return null;
}
}

// nullable or containerDefaultToNull set to true
if (cp.isNullable || containerDefaultToNull) {
return null;
Expand Down Expand Up @@ -1418,12 +1436,31 @@ private String getDefaultCollectionType(Schema schema) {

private String getDefaultCollectionType(Schema schema, String defaultValues) {
String arrayFormat = "new %s<>(Arrays.asList(%s))";

if (defaultToEmptyContainer) {
// respect the default value in the spec
if (defaultValues == null) { // default value not provided
return null;
} else if (defaultValues.isEmpty()) { // e.g. [] to indicates empty container
arrayFormat = "new %s<>()";
return getDefaultCollectionType(arrayFormat, defaultValues, ModelUtils.isSet(schema));
} else { // default value not empty
return getDefaultCollectionType(arrayFormat, defaultValues, ModelUtils.isSet(schema));
}
}

if (defaultValues == null || defaultValues.isEmpty()) {
// default to empty container even though default value is null
// to respect default values provided in the spec, set the option `defaultToEmptyContainer` properly
defaultValues = "";
arrayFormat = "new %s<>()";
}

if (ModelUtils.isSet(schema)) {
return getDefaultCollectionType(arrayFormat, defaultValues, ModelUtils.isSet(schema));
}

private String getDefaultCollectionType(String arrayFormat, String defaultValues, boolean isSet) {
if (isSet) {
return String.format(Locale.ROOT, arrayFormat,
instantiationTypes().getOrDefault("set", "LinkedHashSet"), defaultValues);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5501,4 +5501,35 @@ public void testEnumFieldShouldBeFinal_issue21018() throws IOException {
JavaFileAssert.assertThat(files.get("SomeObject.java"))
.fileContains("private final String value");
}

@Test
public void testCollectionTypesWithDefaults_issue_collection() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();

OpenAPI openAPI = new OpenAPIParser()
.readLocation("src/test/resources/3_0/java/issue_collection.yaml", null, new ParseOptions()).getOpenAPI();
SpringCodegen codegen = new SpringCodegen();
codegen.setLibrary(SPRING_CLOUD_LIBRARY);
codegen.setOutputDir(output.getAbsolutePath());
codegen.additionalProperties().put(CodegenConstants.MODEL_PACKAGE, "xyz.model");
codegen.additionalProperties().put(CodegenConstants.API_NAME_SUFFIX, "Controller");
codegen.additionalProperties().put(CodegenConstants.API_PACKAGE, "xyz.controller");
codegen.additionalProperties().put(CodegenConstants.MODEL_NAME_SUFFIX, "Dto");
codegen.additionalProperties().put("defaultToEmptyContainer", "array");

ClientOptInput input = new ClientOptInput()
.openAPI(openAPI)
.config(codegen);

DefaultGenerator generator = new DefaultGenerator();
Map<String, File> files = generator.opts(input).generate().stream()
.collect(Collectors.toMap(File::getName, Function.identity()));

JavaFileAssert.assertThat(files.get("PetDto.java"))
.fileContains("private @Nullable List<@Valid TagDto> tags;")
.fileContains("private List<@Valid TagDto> tagsRequiredList = new ArrayList<>();")
.fileContains("private @Nullable List<String> stringList;")
.fileContains("private List<String> stringRequiredList = new ArrayList<>();");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
openapi: 3.0.0
servers:
- url: 'http://petstore.swagger.io/v2'
info:
description: >-
This is a sample server Petstore server. For this sample, you can use the api key
`special-key` to test the authorization filters.
version: 1.0.0
title: OpenAPI Petstore
license:
name: Apache-2.0
url: 'https://www.apache.org/licenses/LICENSE-2.0.html'
paths:
'/pet/{petId}':
get:
tags:
- pet
summary: Find pet by ID
description: Returns a single pet
operationId: getPetById
parameters:
- name: petId
in: path
description: ID of pet to return
required: true
schema:
type: integer
format: int64
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
'400':
description: Invalid ID supplied
'404':
description: Pet not found
components:
schemas:
Tag:
title: Pet Tag
description: A tag for a pet
type: object
properties:
id:
type: integer
format: int64
name:
type: string
Pet:
title: a Pet
description: A pet for sale in the pet store
type: object
required:
- tagsRequiredList
- stringRequiredList
properties:
tags:
type: array
items:
$ref: '#/components/schemas/Tag'
tagsRequiredList:
type: array
items:
$ref: '#/components/schemas/Tag'
stringList:
type: array
items:
type: string
stringRequiredList:
type: array
items:
type: string
Loading
Loading