From d63759c1adb60db5b8d99bf7c5ca110064e5176c Mon Sep 17 00:00:00 2001 From: William Cheng Date: Tue, 13 May 2025 15:43:52 +0800 Subject: [PATCH 1/6] add default to empty container option --- bin/configs/java-okhttp-gson.yaml | 1 + .../openapitools/codegen/DefaultCodegen.java | 99 ++++++++++++++++++- .../java/okhttp-gson/api/openapi.yaml | 53 ++++++++-- 3 files changed, 146 insertions(+), 7 deletions(-) diff --git a/bin/configs/java-okhttp-gson.yaml b/bin/configs/java-okhttp-gson.yaml index a5c109e1a38d..2698b0d23f60 100644 --- a/bin/configs/java-okhttp-gson.yaml +++ b/bin/configs/java-okhttp-gson.yaml @@ -10,6 +10,7 @@ parameterNameMappings: _type: underscoreType type_: typeWithUnderscore additionalProperties: + defaultToEmptyContainer: "array?" artifactId: petstore-okhttp-gson hideGenerationTimestamp: true useOneOfDiscriminatorLookup: true diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index 97e859e80c59..e74686c759e1 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -334,6 +334,11 @@ 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; + final String DEFAULT_TO_EMPTY_CONTAINER = "defaultToEmptyContainer"; + final List EMPTY_LIST = new ArrayList(); + @Override public boolean getAddSuffixToDuplicateOperationNicknames() { return addSuffixToDuplicateOperationNicknames; @@ -392,8 +397,11 @@ 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)); + } + } /*** * Preset map builder with commonly used Mustache lambdas. @@ -4226,6 +4234,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 && additionalProperties.containsKey("defaultToEmptyContainer")) { + updateDefaultToEmptyContainer(property, p); + } + // set the default value property.defaultValue = toDefaultValue(property, p); property.defaultValueWithParam = toDefaultValueWithParam(name, p); @@ -4235,6 +4248,90 @@ 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); + } + } + } + + } + /** * Update property for array(list) container * diff --git a/samples/client/petstore/java/okhttp-gson/api/openapi.yaml b/samples/client/petstore/java/okhttp-gson/api/openapi.yaml index 7c6f5a5ec3ce..176a7291e3a3 100644 --- a/samples/client/petstore/java/okhttp-gson/api/openapi.yaml +++ b/samples/client/petstore/java/okhttp-gson/api/openapi.yaml @@ -108,6 +108,7 @@ paths: name: status required: true schema: + default: [] items: default: available enum: @@ -122,11 +123,13 @@ paths: content: application/xml: schema: + default: [] items: $ref: '#/components/schemas/Pet' type: array application/json: schema: + default: [] items: $ref: '#/components/schemas/Pet' type: array @@ -156,6 +159,7 @@ paths: name: tags required: true schema: + default: [] items: type: string type: array @@ -165,11 +169,13 @@ paths: content: application/xml: schema: + default: [] items: $ref: '#/components/schemas/Pet' type: array application/json: schema: + default: [] items: $ref: '#/components/schemas/Pet' type: array @@ -739,6 +745,7 @@ paths: name: enum_header_string_array required: false schema: + default: [] items: default: $ enum: @@ -766,6 +773,7 @@ paths: name: enum_query_string_array required: false schema: + default: [] items: default: $ enum: @@ -1195,6 +1203,7 @@ paths: name: pipe required: true schema: + default: [] items: type: string type: array @@ -1204,6 +1213,7 @@ paths: name: ioutil required: true schema: + default: [] items: type: string type: array @@ -1213,6 +1223,7 @@ paths: name: http required: true schema: + default: [] items: type: string type: array @@ -1222,6 +1233,7 @@ paths: name: url required: true schema: + default: [] items: type: string type: array @@ -1231,6 +1243,7 @@ paths: name: context required: true schema: + default: [] items: type: string type: array @@ -1558,6 +1571,7 @@ components: - username: foo - username: bar schema: + default: [] items: $ref: '#/components/schemas/User' type: array @@ -1748,6 +1762,7 @@ components: name: photoUrl wrapped: true tags: + default: [] items: $ref: '#/components/schemas/Tag' type: array @@ -2102,18 +2117,23 @@ components: ArrayTest: properties: array_of_string: + default: [] items: type: string type: array array_array_of_integer: + default: [] items: + default: [] items: format: int64 type: integer type: array type: array array_array_of_model: + default: [] items: + default: [] items: $ref: '#/components/schemas/ReadOnlyFirst' type: array @@ -2127,6 +2147,7 @@ components: ArrayOfNumberOnly: properties: ArrayNumber: + default: [] items: type: number type: array @@ -2134,7 +2155,9 @@ components: ArrayOfArrayOfNumberOnly: properties: ArrayArrayNumber: + default: [] items: + default: [] items: type: number type: array @@ -2148,6 +2171,7 @@ components: - $ type: string array_enum: + default: [] items: enum: - fish @@ -2229,6 +2253,7 @@ components: file: $ref: '#/components/schemas/File' files: + default: [] items: $ref: '#/components/schemas/File' type: array @@ -2308,6 +2333,7 @@ components: nullable: true type: array array_items_nullable: + default: [] items: nullable: true type: object @@ -2456,6 +2482,7 @@ components: nullableShape: $ref: '#/components/schemas/NullableShape' shapes: + default: [] items: $ref: '#/components/schemas/Shape' type: array @@ -2551,6 +2578,7 @@ components: - $ref: '#/components/schemas/GrandparentAnimal' type: object ArrayOfEnums: + default: [] items: $ref: '#/components/schemas/OuterEnum' type: array @@ -2575,6 +2603,7 @@ components: deprecatedRef: $ref: '#/components/schemas/DeprecatedObject' bars: + default: [] deprecated: true items: $ref: '#/components/schemas/Bar' @@ -2628,6 +2657,7 @@ components: example: doggie type: string array_allof_dog_property: + default: [] items: $ref: '#/components/schemas/ArrayOfInlineAllOf_array_allof_dog_property_inner' type: array @@ -2656,6 +2686,7 @@ components: type: string type: array WithoutDefault: + default: [] items: type: string type: array @@ -2692,6 +2723,7 @@ components: - type: boolean description: Values of scalar type using anyOf Array: + default: [] description: Values of array type items: $ref: '#/components/schemas/Scalar' @@ -2730,6 +2762,7 @@ components: name: photoUrl wrapped: true tags: + default: [] items: allOf: - $ref: '#/components/schemas/Tag' @@ -2797,6 +2830,7 @@ components: name: photoUrl wrapped: true tags: + default: [] items: allOf: - $ref: '#/components/schemas/Tag' @@ -2841,13 +2875,15 @@ components: ArrayOneOf: oneOf: - type: integer - - items: + - default: [] + items: type: string type: array ArrayAnyOf: anyOf: - type: integer - - items: + - default: [] + items: type: string type: array ModelWithOneOfAnyOfProperties: @@ -2887,6 +2923,7 @@ components: testEnumParameters_request: properties: enum_form_string_array: + default: [] description: Form parameter enum test (string array) items: default: $ @@ -2992,18 +3029,22 @@ components: type: object _fake_oneOfWIthSameErasure_get_200_response: oneOf: - - items: + - default: [] + items: type: string type: array - - items: + - default: [] + items: type: integer type: array _fake_anyOfWIthSameErasure_get_200_response: anyOf: - - items: + - default: [] + items: type: string type: array - - items: + - default: [] + items: type: integer type: array testInlineFreeformAdditionalProperties_request: From 4974932cf25b0247cac59dbf8b239b94d6ef577d Mon Sep 17 00:00:00 2001 From: William Cheng Date: Tue, 13 May 2025 15:32:37 +0800 Subject: [PATCH 2/6] test map default to empty container --- bin/configs/java-okhttp-gson.yaml | 2 +- .../java/okhttp-gson/api/openapi.yaml | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/bin/configs/java-okhttp-gson.yaml b/bin/configs/java-okhttp-gson.yaml index 2698b0d23f60..95e38d0f9814 100644 --- a/bin/configs/java-okhttp-gson.yaml +++ b/bin/configs/java-okhttp-gson.yaml @@ -10,7 +10,7 @@ parameterNameMappings: _type: underscoreType type_: typeWithUnderscore additionalProperties: - defaultToEmptyContainer: "array?" + defaultToEmptyContainer: "array?|map?" artifactId: petstore-okhttp-gson hideGenerationTimestamp: true useOneOfDiscriminatorLookup: true diff --git a/samples/client/petstore/java/okhttp-gson/api/openapi.yaml b/samples/client/petstore/java/okhttp-gson/api/openapi.yaml index 176a7291e3a3..8aa109536dcc 100644 --- a/samples/client/petstore/java/okhttp-gson/api/openapi.yaml +++ b/samples/client/petstore/java/okhttp-gson/api/openapi.yaml @@ -348,6 +348,7 @@ paths: additionalProperties: format: int32 type: integer + default: [] type: object description: successful operation security: @@ -1062,6 +1063,7 @@ paths: schema: additionalProperties: type: string + default: [] type: object description: request body required: true @@ -1856,6 +1858,7 @@ components: Address: additionalProperties: type: integer + default: [] type: object Animal: discriminator: @@ -2005,12 +2008,15 @@ components: map_property: additionalProperties: type: string + default: [] type: object map_of_map_property: additionalProperties: additionalProperties: type: string + default: [] type: object + default: [] type: object anytype_1: {} map_with_undeclared_properties_anytype_1: @@ -2020,6 +2026,7 @@ components: type: object map_with_undeclared_properties_anytype_3: additionalProperties: true + default: [] type: object empty_map: additionalProperties: false @@ -2029,6 +2036,7 @@ components: map_with_undeclared_properties_string: additionalProperties: type: string + default: [] type: object type: object MixedPropertiesAndAdditionalPropertiesClass: @@ -2042,6 +2050,7 @@ components: map: additionalProperties: $ref: '#/components/schemas/Animal' + default: [] type: object type: object List: @@ -2096,7 +2105,9 @@ components: additionalProperties: additionalProperties: type: string + default: [] type: object + default: [] type: object map_of_enum_string: additionalProperties: @@ -2104,14 +2115,17 @@ components: - UPPER - lower type: string + default: [] type: object direct_map: additionalProperties: type: boolean + default: [] type: object indirect_map: additionalProperties: type: boolean + default: [] type: object type: object ArrayTest: @@ -2181,11 +2195,13 @@ components: type: object FreeFormObject: additionalProperties: true + default: [] description: A schema consisting only of additional properties type: object MapOfString: additionalProperties: type: string + default: [] description: A schema consisting only of additional properties of type string type: object OuterEnum: @@ -2241,6 +2257,7 @@ components: StringBooleanMap: additionalProperties: type: boolean + default: [] type: object FileSchemaTestClass: example: @@ -2353,6 +2370,7 @@ components: additionalProperties: nullable: true type: object + default: [] type: object type: object fruit: @@ -3073,6 +3091,7 @@ components: oneOf: - type: string - additionalProperties: true + default: [] type: object ArrayOfInlineAllOf_array_allof_dog_property_inner: allOf: From 3dcb59efb44e6a2b1a96b6dddcf25a3fe07dd2ca Mon Sep 17 00:00:00 2001 From: William Cheng Date: Sun, 18 May 2025 22:40:05 +0800 Subject: [PATCH 3/6] update java generators to respect default value --- .../org/openapitools/codegen/DefaultCodegen.java | 5 ++++- .../codegen/languages/AbstractJavaCodegen.java | 15 ++++++++++++++- .../AllOfModelArrayAnyOfAllOfLinkListColumn1.java | 2 +- .../org/openapitools/client/model/NewPet.java | 2 +- .../java/org/openapitools/client/model/Pet.java | 2 +- .../openapitools/client/model/PetComposition.java | 2 +- .../org/openapitools/client/model/PetRef.java | 2 +- .../openapitools/client/model/PetUsingAllOf.java | 2 +- .../client/model/PetWithRequiredTags.java | 4 ++-- 9 files changed, 26 insertions(+), 10 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index e74686c759e1..afdb7e5ca372 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -336,6 +336,7 @@ apiTemplateFiles are for API outputs only (controllers/handlers). @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(); @@ -4235,7 +4236,7 @@ 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 && additionalProperties.containsKey("defaultToEmptyContainer")) { + if (p.getDefault() == null && defaultToEmptyContainer) { updateDefaultToEmptyContainer(property, p); } @@ -4328,6 +4329,8 @@ void parseDefaultToEmptyContainer(String input) { LOGGER.error("Skipped invalid container type `{}` in the rule `{}`.", containerType, input); } } + + defaultToEmptyContainer = true; } } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java index 8a624b0bbf86..2af326861579 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java @@ -1306,7 +1306,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; @@ -1323,6 +1326,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; diff --git a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/AllOfModelArrayAnyOfAllOfLinkListColumn1.java b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/AllOfModelArrayAnyOfAllOfLinkListColumn1.java index 67e9515340ab..91e6444666ce 100644 --- a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/AllOfModelArrayAnyOfAllOfLinkListColumn1.java +++ b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/AllOfModelArrayAnyOfAllOfLinkListColumn1.java @@ -56,7 +56,7 @@ public class AllOfModelArrayAnyOfAllOfLinkListColumn1 { public static final String SERIALIZED_NAME_VALUE = "value"; @SerializedName(SERIALIZED_NAME_VALUE) @javax.annotation.Nonnull - private List value = new ArrayList<>(); + private List value; public AllOfModelArrayAnyOfAllOfLinkListColumn1() { } diff --git a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/NewPet.java b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/NewPet.java index 6ee8a25bdbfc..9a2da53378a0 100644 --- a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/NewPet.java +++ b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/NewPet.java @@ -88,7 +88,7 @@ public class NewPet { public static final String SERIALIZED_NAME_PHOTO_URLS = "photoUrls"; @SerializedName(SERIALIZED_NAME_PHOTO_URLS) @javax.annotation.Nonnull - private List photoUrls = new ArrayList<>(); + private List photoUrls; public static final String SERIALIZED_NAME_TAGS = "tags"; @SerializedName(SERIALIZED_NAME_TAGS) diff --git a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/Pet.java b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/Pet.java index 2cbbfdbb45a6..664ee6197120 100644 --- a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/Pet.java +++ b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/Pet.java @@ -72,7 +72,7 @@ public class Pet { public static final String SERIALIZED_NAME_PHOTO_URLS = "photoUrls"; @SerializedName(SERIALIZED_NAME_PHOTO_URLS) @javax.annotation.Nonnull - private List photoUrls = new ArrayList<>(); + private List photoUrls; public static final String SERIALIZED_NAME_TAGS = "tags"; @SerializedName(SERIALIZED_NAME_TAGS) diff --git a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetComposition.java b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetComposition.java index 59add75ea68e..aeb560740847 100644 --- a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetComposition.java +++ b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetComposition.java @@ -72,7 +72,7 @@ public class PetComposition { public static final String SERIALIZED_NAME_PHOTO_URLS = "photoUrls"; @SerializedName(SERIALIZED_NAME_PHOTO_URLS) @javax.annotation.Nonnull - private List photoUrls = new ArrayList<>(); + private List photoUrls; public static final String SERIALIZED_NAME_TAGS = "tags"; @SerializedName(SERIALIZED_NAME_TAGS) diff --git a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetRef.java b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetRef.java index 019ea3cfdc90..c2e3b5f2fa74 100644 --- a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetRef.java +++ b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetRef.java @@ -72,7 +72,7 @@ public class PetRef { public static final String SERIALIZED_NAME_PHOTO_URLS = "photoUrls"; @SerializedName(SERIALIZED_NAME_PHOTO_URLS) @javax.annotation.Nonnull - private List photoUrls = new ArrayList<>(); + private List photoUrls; public static final String SERIALIZED_NAME_TAGS = "tags"; @SerializedName(SERIALIZED_NAME_TAGS) diff --git a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetUsingAllOf.java b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetUsingAllOf.java index d9dd26116902..7b498909fd9e 100644 --- a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetUsingAllOf.java +++ b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetUsingAllOf.java @@ -72,7 +72,7 @@ public class PetUsingAllOf { public static final String SERIALIZED_NAME_PHOTO_URLS = "photoUrls"; @SerializedName(SERIALIZED_NAME_PHOTO_URLS) @javax.annotation.Nonnull - private List photoUrls = new ArrayList<>(); + private List photoUrls; public static final String SERIALIZED_NAME_TAGS = "tags"; @SerializedName(SERIALIZED_NAME_TAGS) diff --git a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetWithRequiredTags.java b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetWithRequiredTags.java index 0eb3e1ebab78..26ca173b6dcc 100644 --- a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetWithRequiredTags.java +++ b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetWithRequiredTags.java @@ -72,12 +72,12 @@ public class PetWithRequiredTags { public static final String SERIALIZED_NAME_PHOTO_URLS = "photoUrls"; @SerializedName(SERIALIZED_NAME_PHOTO_URLS) @javax.annotation.Nonnull - private List photoUrls = new ArrayList<>(); + private List photoUrls; public static final String SERIALIZED_NAME_TAGS = "tags"; @SerializedName(SERIALIZED_NAME_TAGS) @javax.annotation.Nonnull - private List tags = new ArrayList<>(); + private List tags; /** * pet status in the store From 52960b8c23106eba1d769f0592a7aef27f3f959f Mon Sep 17 00:00:00 2001 From: William Cheng Date: Mon, 19 May 2025 00:22:27 +0800 Subject: [PATCH 4/6] various fixes --- bin/configs/java-okhttp-gson.yaml | 2 +- .../openapitools/codegen/DefaultCodegen.java | 12 ++- .../languages/AbstractJavaCodegen.java | 28 ++++++- .../java/spring/SpringCodegenTest.java | 31 ++++++++ .../resources/3_0/java/issue_collection.yaml | 75 +++++++++++++++++++ .../java/okhttp-gson/api/openapi.yaml | 6 ++ ...OfModelArrayAnyOfAllOfLinkListColumn1.java | 2 +- .../org/openapitools/client/model/NewPet.java | 2 +- .../org/openapitools/client/model/Pet.java | 2 +- .../client/model/PetComposition.java | 2 +- .../org/openapitools/client/model/PetRef.java | 2 +- .../client/model/PetUsingAllOf.java | 2 +- .../client/model/PetWithRequiredTags.java | 4 +- 13 files changed, 157 insertions(+), 13 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/java/issue_collection.yaml diff --git a/bin/configs/java-okhttp-gson.yaml b/bin/configs/java-okhttp-gson.yaml index 95e38d0f9814..f404858c2776 100644 --- a/bin/configs/java-okhttp-gson.yaml +++ b/bin/configs/java-okhttp-gson.yaml @@ -10,7 +10,7 @@ parameterNameMappings: _type: underscoreType type_: typeWithUnderscore additionalProperties: - defaultToEmptyContainer: "array?|map?" + defaultToEmptyContainer: "array?|array|map?" artifactId: petstore-okhttp-gson hideGenerationTimestamp: true useOneOfDiscriminatorLookup: true diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index afdb7e5ca372..33918bed8462 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -401,6 +401,7 @@ public void processOpts() { 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; } } @@ -4328,11 +4329,18 @@ void parseDefaultToEmptyContainer(String input) { } 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); + } } - defaultToEmptyContainer = true; } - } /** diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java index 2af326861579..234ba57ceb74 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java @@ -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 final_values = _values; @@ -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())); } @@ -1431,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); } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java index 328a8acdb348..67cfe57679b6 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java @@ -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 files = generator.opts(input).generate().stream() + .collect(Collectors.toMap(File::getName, Function.identity())); + + JavaFileAssert.assertThat(files.get("PetDto.java")) + .fileContains("private List<@Valid TagDto> tags;") + .fileContains("private List<@Valid TagDto> tagsRequiredList = new ArrayList<>();") + .fileContains("private List stringList;") + .fileContains("private List stringRequiredList = new ArrayList<>();"); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/java/issue_collection.yaml b/modules/openapi-generator/src/test/resources/3_0/java/issue_collection.yaml new file mode 100644 index 000000000000..3ec1aa6ff213 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/java/issue_collection.yaml @@ -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 diff --git a/samples/client/petstore/java/okhttp-gson/api/openapi.yaml b/samples/client/petstore/java/okhttp-gson/api/openapi.yaml index 8aa109536dcc..ff7831f581a2 100644 --- a/samples/client/petstore/java/okhttp-gson/api/openapi.yaml +++ b/samples/client/petstore/java/okhttp-gson/api/openapi.yaml @@ -1757,6 +1757,7 @@ components: example: doggie type: string photoUrls: + default: [] items: type: string type: array @@ -2639,6 +2640,7 @@ components: example: doggie type: string photoUrls: + default: [] items: type: string type: array @@ -2646,6 +2648,7 @@ components: name: photoUrl wrapped: true tags: + default: [] items: $ref: '#/components/schemas/Tag' type: array @@ -2773,6 +2776,7 @@ components: example: doggie type: string photoUrls: + default: [] items: type: string type: array @@ -2841,6 +2845,7 @@ components: example: doggie type: string photoUrls: + default: [] items: type: string type: array @@ -3132,6 +3137,7 @@ components: AllOfModelArrayAnyOf_allOf_linkListColumn1: properties: value: + default: [] items: $ref: '#/components/schemas/AllOfModelArrayAnyOf_allOf_linkListColumn1_value' type: array diff --git a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/AllOfModelArrayAnyOfAllOfLinkListColumn1.java b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/AllOfModelArrayAnyOfAllOfLinkListColumn1.java index 91e6444666ce..67e9515340ab 100644 --- a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/AllOfModelArrayAnyOfAllOfLinkListColumn1.java +++ b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/AllOfModelArrayAnyOfAllOfLinkListColumn1.java @@ -56,7 +56,7 @@ public class AllOfModelArrayAnyOfAllOfLinkListColumn1 { public static final String SERIALIZED_NAME_VALUE = "value"; @SerializedName(SERIALIZED_NAME_VALUE) @javax.annotation.Nonnull - private List value; + private List value = new ArrayList<>(); public AllOfModelArrayAnyOfAllOfLinkListColumn1() { } diff --git a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/NewPet.java b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/NewPet.java index 9a2da53378a0..6ee8a25bdbfc 100644 --- a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/NewPet.java +++ b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/NewPet.java @@ -88,7 +88,7 @@ public class NewPet { public static final String SERIALIZED_NAME_PHOTO_URLS = "photoUrls"; @SerializedName(SERIALIZED_NAME_PHOTO_URLS) @javax.annotation.Nonnull - private List photoUrls; + private List photoUrls = new ArrayList<>(); public static final String SERIALIZED_NAME_TAGS = "tags"; @SerializedName(SERIALIZED_NAME_TAGS) diff --git a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/Pet.java b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/Pet.java index 664ee6197120..2cbbfdbb45a6 100644 --- a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/Pet.java +++ b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/Pet.java @@ -72,7 +72,7 @@ public class Pet { public static final String SERIALIZED_NAME_PHOTO_URLS = "photoUrls"; @SerializedName(SERIALIZED_NAME_PHOTO_URLS) @javax.annotation.Nonnull - private List photoUrls; + private List photoUrls = new ArrayList<>(); public static final String SERIALIZED_NAME_TAGS = "tags"; @SerializedName(SERIALIZED_NAME_TAGS) diff --git a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetComposition.java b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetComposition.java index aeb560740847..59add75ea68e 100644 --- a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetComposition.java +++ b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetComposition.java @@ -72,7 +72,7 @@ public class PetComposition { public static final String SERIALIZED_NAME_PHOTO_URLS = "photoUrls"; @SerializedName(SERIALIZED_NAME_PHOTO_URLS) @javax.annotation.Nonnull - private List photoUrls; + private List photoUrls = new ArrayList<>(); public static final String SERIALIZED_NAME_TAGS = "tags"; @SerializedName(SERIALIZED_NAME_TAGS) diff --git a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetRef.java b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetRef.java index c2e3b5f2fa74..019ea3cfdc90 100644 --- a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetRef.java +++ b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetRef.java @@ -72,7 +72,7 @@ public class PetRef { public static final String SERIALIZED_NAME_PHOTO_URLS = "photoUrls"; @SerializedName(SERIALIZED_NAME_PHOTO_URLS) @javax.annotation.Nonnull - private List photoUrls; + private List photoUrls = new ArrayList<>(); public static final String SERIALIZED_NAME_TAGS = "tags"; @SerializedName(SERIALIZED_NAME_TAGS) diff --git a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetUsingAllOf.java b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetUsingAllOf.java index 7b498909fd9e..d9dd26116902 100644 --- a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetUsingAllOf.java +++ b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetUsingAllOf.java @@ -72,7 +72,7 @@ public class PetUsingAllOf { public static final String SERIALIZED_NAME_PHOTO_URLS = "photoUrls"; @SerializedName(SERIALIZED_NAME_PHOTO_URLS) @javax.annotation.Nonnull - private List photoUrls; + private List photoUrls = new ArrayList<>(); public static final String SERIALIZED_NAME_TAGS = "tags"; @SerializedName(SERIALIZED_NAME_TAGS) diff --git a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetWithRequiredTags.java b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetWithRequiredTags.java index 26ca173b6dcc..0eb3e1ebab78 100644 --- a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetWithRequiredTags.java +++ b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/PetWithRequiredTags.java @@ -72,12 +72,12 @@ public class PetWithRequiredTags { public static final String SERIALIZED_NAME_PHOTO_URLS = "photoUrls"; @SerializedName(SERIALIZED_NAME_PHOTO_URLS) @javax.annotation.Nonnull - private List photoUrls; + private List photoUrls = new ArrayList<>(); public static final String SERIALIZED_NAME_TAGS = "tags"; @SerializedName(SERIALIZED_NAME_TAGS) @javax.annotation.Nonnull - private List tags; + private List tags = new ArrayList<>(); /** * pet status in the store From d7e33efedc8909b38db25639234d8203dad23930 Mon Sep 17 00:00:00 2001 From: William Cheng Date: Mon, 19 May 2025 01:05:48 +0800 Subject: [PATCH 5/6] fix tests --- .../openapitools/codegen/java/spring/SpringCodegenTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java index 67cfe57679b6..30a7c092d8c9 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java @@ -5527,9 +5527,9 @@ public void testCollectionTypesWithDefaults_issue_collection() throws IOExceptio .collect(Collectors.toMap(File::getName, Function.identity())); JavaFileAssert.assertThat(files.get("PetDto.java")) - .fileContains("private List<@Valid TagDto> tags;") + .fileContains("private @Nullable List<@Valid TagDto> tags;") .fileContains("private List<@Valid TagDto> tagsRequiredList = new ArrayList<>();") - .fileContains("private List stringList;") + .fileContains("private @Nullable List stringList;") .fileContains("private List stringRequiredList = new ArrayList<>();"); } } From bfc4bdb35a5eba8a63f92471b150382e08f42260 Mon Sep 17 00:00:00 2001 From: William Cheng Date: Mon, 26 May 2025 10:29:54 +0800 Subject: [PATCH 6/6] update doc --- docs/customization.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/customization.md b/docs/customization.md index a7be58f96b91..bf0958c8b964 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -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: