From ee73d81c0a2863ff571e70f8a0fbf8236a1aed34 Mon Sep 17 00:00:00 2001 From: Subham Ashok Date: Tue, 4 Aug 2026 00:51:52 +0530 Subject: [PATCH 1/4] [core] keep property type when an allOf part redefines it without a type An allOf part that only adds constraints to an inherited property, for example 'nullable: true', replaced the typed schema wholesale and the property degraded to Object. Merge the constraints onto the existing typed schema instead. Fixes #4128 for OpenAPI 3.x input. Swagger 2.0 input still degrades because the 2.0 to 3.0 converter itself injects 'type: object' before the generator runs. Co-Authored-By: Claude Opus 4.7 --- .../openapitools/codegen/DefaultCodegen.java | 60 ++++++++++++++++++- .../codegen/DefaultCodegenTest.java | 17 ++++++ .../3_0/allOf-nullable-typeless-override.yaml | 38 ++++++++++++ 3 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/allOf-nullable-typeless-override.yaml 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 945d828d9295..a284530ba5c9 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 @@ -2941,7 +2941,7 @@ private void mergeProperties(Map existingProperties, Map - existingProperties.put(key, ModelUtils.cloneSchema(value, specVersionGreaterThanOrEqualTo310(openAPI))) + putProperty(existingProperties, key, ModelUtils.cloneSchema(value, specVersionGreaterThanOrEqualTo310(openAPI))) ); if (null != existingType && null != newType && null != newType.getEnum() && !newType.getEnum().isEmpty()) { for (Object e : newType.getEnum()) { @@ -3608,7 +3608,7 @@ protected void addProperties(Map properties, List requir if (ModelUtils.isComposedSchema(schema)) { // fix issue #16797 and #15796, constructor fail by missing parent required params if (ModelUtils.hasProperties(schema)) { - properties.putAll(schema.getProperties()); + putProperties(properties, schema.getProperties()); } if (schema.getAllOf() != null) { @@ -3642,13 +3642,67 @@ protected void addProperties(Map properties, List requir return; } if (schema.getProperties() != null) { - properties.putAll(schema.getProperties()); + putProperties(properties, schema.getProperties()); } if (schema.getRequired() != null) { required.addAll(schema.getRequired()); } } + /** + * Adds each property to the target map. When a property of the same name is already present + * with type information and the incoming schema carries no type of its own (for example an + * allOf part that only sets 'nullable: true' on an inherited property), the incoming + * constraints are applied on top of the existing schema instead of replacing it, so the + * type is not lost. See issue #4128. + */ + private void putProperties(Map targetProperties, Map newProperties) { + newProperties.forEach((name, incoming) -> putProperty(targetProperties, name, incoming)); + } + + private void putProperty(Map targetProperties, String name, Schema incoming) { + Schema existing = targetProperties.get(name); + if (existing != null && incoming != null + && !ModelUtils.isAnyType(existing) && isConstraintOnlySchema(incoming)) { + Schema merged = ModelUtils.cloneSchema(existing, specVersionGreaterThanOrEqualTo310(openAPI)); + if (incoming.getNullable() != null) { + merged.setNullable(incoming.getNullable()); + } + if (incoming.getDescription() != null) { + merged.setDescription(incoming.getDescription()); + } + if (incoming.getDeprecated() != null) { + merged.setDeprecated(incoming.getDeprecated()); + } + if (incoming.getReadOnly() != null) { + merged.setReadOnly(incoming.getReadOnly()); + } + if (incoming.getWriteOnly() != null) { + merged.setWriteOnly(incoming.getWriteOnly()); + } + if (incoming.getExtensions() != null) { + incoming.getExtensions().forEach((k, v) -> merged.addExtension(String.valueOf(k), v)); + } + targetProperties.put(name, merged); + } else { + targetProperties.put(name, incoming); + } + } + + /** + * True when the schema defines no type of its own: no type, no $ref, no items, + * no properties, no composition and no enum. + */ + private static boolean isConstraintOnlySchema(Schema schema) { + return ModelUtils.isAnyType(schema) + && schema.getItems() == null + && schema.getProperties() == null + && schema.getAllOf() == null + && schema.getOneOf() == null + && schema.getAnyOf() == null + && schema.getEnum() == null; + } + /** * Camelize the method name of the getter and setter * diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java index dbc777e2ca15..671cf8b10bfc 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java @@ -1314,6 +1314,23 @@ public void testAllOfRequired() { assertEquals(getRequiredVars(childModel), Collections.singletonList("name")); } + @Test + public void testAllOfNullableWithoutTypeKeepsType() { + // issue #4128: an allOf part that only sets 'nullable: true' on a property + // defined in another part must not erase the property type + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/allOf-nullable-typeless-override.yaml"); + DefaultCodegen codegen = new DefaultCodegen(); + codegen.setOpenAPI(openAPI); + + Schema schema = openAPI.getComponents().getSchemas().get("UpdateFirm"); + CodegenModel model = codegen.fromModel("UpdateFirm", schema); + + CodegenProperty addressId = model.vars.stream() + .filter(v -> "addressId".equals(v.baseName)).findFirst().orElseThrow(); + assertEquals("String", addressId.dataType); + assertTrue(addressId.isNullable); + } + @Test public void testAllOfSingleAndDoubleRefWithOwnPropsNoDiscriminator() { final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/allOf_composition.yaml"); diff --git a/modules/openapi-generator/src/test/resources/3_0/allOf-nullable-typeless-override.yaml b/modules/openapi-generator/src/test/resources/3_0/allOf-nullable-typeless-override.yaml new file mode 100644 index 000000000000..50e4a33f2f01 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/allOf-nullable-typeless-override.yaml @@ -0,0 +1,38 @@ +openapi: 3.0.3 +info: + title: allOf nullable without type + version: 1.0.0 +paths: + /firm/{firmId}: + patch: + operationId: updateFirm + parameters: + - in: path + name: firmId + required: true + schema: + type: integer + format: int64 + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateFirm" + responses: + "204": + description: Updated +components: + schemas: + FirmProperties: + properties: + addressId: + type: string + UpdateFirm: + allOf: + - $ref: "#/components/schemas/FirmProperties" + - properties: + firmName: + type: string + nullable: true + addressId: + nullable: true From 90a7c321075859ab0fbd2964c47ad1dd4546ec1c Mon Sep 17 00:00:00 2001 From: Subham Ashok Date: Tue, 4 Aug 2026 15:41:40 +0530 Subject: [PATCH 2/4] copy validation and format keywords in the allOf property merge Review feedback: the merge kept the type but only carried a few constraint fields. Copy the remaining validation, format and metadata keywords too, so no declared constraint is dropped. Co-Authored-By: Claude Opus 4.7 --- .../openapitools/codegen/DefaultCodegen.java | 62 ++++++++++++++++++- .../codegen/DefaultCodegenTest.java | 1 + .../3_0/allOf-nullable-typeless-override.yaml | 1 + 3 files changed, 62 insertions(+), 2 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 a284530ba5c9..799fd9cd2383 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 @@ -3653,8 +3653,9 @@ protected void addProperties(Map properties, List requir * Adds each property to the target map. When a property of the same name is already present * with type information and the incoming schema carries no type of its own (for example an * allOf part that only sets 'nullable: true' on an inherited property), the incoming - * constraints are applied on top of the existing schema instead of replacing it, so the - * type is not lost. See issue #4128. + * constraints (nullable, description, validation keywords, format, default, extensions) + * are applied on top of the existing schema instead of replacing it, so the type is not + * lost. See issue #4128. */ private void putProperties(Map targetProperties, Map newProperties) { newProperties.forEach((name, incoming) -> putProperty(targetProperties, name, incoming)); @@ -3680,6 +3681,63 @@ private void putProperty(Map targetProperties, String name, Sche if (incoming.getWriteOnly() != null) { merged.setWriteOnly(incoming.getWriteOnly()); } + if (incoming.getTitle() != null) { + merged.setTitle(incoming.getTitle()); + } + if (incoming.getFormat() != null) { + merged.setFormat(incoming.getFormat()); + } + if (incoming.getDefault() != null) { + merged.setDefault(incoming.getDefault()); + } + if (incoming.getExample() != null) { + merged.setExample(incoming.getExample()); + } + if (incoming.getPattern() != null) { + merged.setPattern(incoming.getPattern()); + } + if (incoming.getMaxLength() != null) { + merged.setMaxLength(incoming.getMaxLength()); + } + if (incoming.getMinLength() != null) { + merged.setMinLength(incoming.getMinLength()); + } + if (incoming.getMaximum() != null) { + merged.setMaximum(incoming.getMaximum()); + } + if (incoming.getMinimum() != null) { + merged.setMinimum(incoming.getMinimum()); + } + if (incoming.getExclusiveMaximum() != null) { + merged.setExclusiveMaximum(incoming.getExclusiveMaximum()); + } + if (incoming.getExclusiveMinimum() != null) { + merged.setExclusiveMinimum(incoming.getExclusiveMinimum()); + } + if (incoming.getExclusiveMaximumValue() != null) { + merged.setExclusiveMaximumValue(incoming.getExclusiveMaximumValue()); + } + if (incoming.getExclusiveMinimumValue() != null) { + merged.setExclusiveMinimumValue(incoming.getExclusiveMinimumValue()); + } + if (incoming.getMultipleOf() != null) { + merged.setMultipleOf(incoming.getMultipleOf()); + } + if (incoming.getMaxItems() != null) { + merged.setMaxItems(incoming.getMaxItems()); + } + if (incoming.getMinItems() != null) { + merged.setMinItems(incoming.getMinItems()); + } + if (incoming.getUniqueItems() != null) { + merged.setUniqueItems(incoming.getUniqueItems()); + } + if (incoming.getMaxProperties() != null) { + merged.setMaxProperties(incoming.getMaxProperties()); + } + if (incoming.getMinProperties() != null) { + merged.setMinProperties(incoming.getMinProperties()); + } if (incoming.getExtensions() != null) { incoming.getExtensions().forEach((k, v) -> merged.addExtension(String.valueOf(k), v)); } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java index 671cf8b10bfc..0ed1011f1c0d 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java @@ -1329,6 +1329,7 @@ public void testAllOfNullableWithoutTypeKeepsType() { .filter(v -> "addressId".equals(v.baseName)).findFirst().orElseThrow(); assertEquals("String", addressId.dataType); assertTrue(addressId.isNullable); + assertEquals(Integer.valueOf(36), addressId.maxLength); } @Test diff --git a/modules/openapi-generator/src/test/resources/3_0/allOf-nullable-typeless-override.yaml b/modules/openapi-generator/src/test/resources/3_0/allOf-nullable-typeless-override.yaml index 50e4a33f2f01..9a40272bd4c6 100644 --- a/modules/openapi-generator/src/test/resources/3_0/allOf-nullable-typeless-override.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/allOf-nullable-typeless-override.yaml @@ -36,3 +36,4 @@ components: nullable: true addressId: nullable: true + maxLength: 36 From 2a9ebcb41bc9c84b2ea67ab8c29e92ddb92c640c Mon Sep 17 00:00:00 2001 From: Subham Ashok Date: Tue, 4 Aug 2026 16:46:11 +0530 Subject: [PATCH 3/4] move constraint copying to ModelUtils next to copyMetadata Review feedback: reuse copyMetadata instead of a second hand-written copy list. New ModelUtils.copyConstraints delegates to copyMetadata and adds the validation keywords it does not cover. Guard now uses the existing isMetadataOnlySchema. Co-Authored-By: Claude Opus 4.7 --- .../openapitools/codegen/DefaultCodegen.java | 93 +------------------ .../codegen/utils/ModelUtils.java | 42 +++++++++ 2 files changed, 45 insertions(+), 90 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 799fd9cd2383..2b0bdfaf0b60 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 @@ -3664,103 +3664,16 @@ private void putProperties(Map targetProperties, Map targetProperties, String name, Schema incoming) { Schema existing = targetProperties.get(name); if (existing != null && incoming != null - && !ModelUtils.isAnyType(existing) && isConstraintOnlySchema(incoming)) { + && !ModelUtils.isAnyType(existing) + && ModelUtils.isMetadataOnlySchema(incoming) && incoming.getEnum() == null) { Schema merged = ModelUtils.cloneSchema(existing, specVersionGreaterThanOrEqualTo310(openAPI)); - if (incoming.getNullable() != null) { - merged.setNullable(incoming.getNullable()); - } - if (incoming.getDescription() != null) { - merged.setDescription(incoming.getDescription()); - } - if (incoming.getDeprecated() != null) { - merged.setDeprecated(incoming.getDeprecated()); - } - if (incoming.getReadOnly() != null) { - merged.setReadOnly(incoming.getReadOnly()); - } - if (incoming.getWriteOnly() != null) { - merged.setWriteOnly(incoming.getWriteOnly()); - } - if (incoming.getTitle() != null) { - merged.setTitle(incoming.getTitle()); - } - if (incoming.getFormat() != null) { - merged.setFormat(incoming.getFormat()); - } - if (incoming.getDefault() != null) { - merged.setDefault(incoming.getDefault()); - } - if (incoming.getExample() != null) { - merged.setExample(incoming.getExample()); - } - if (incoming.getPattern() != null) { - merged.setPattern(incoming.getPattern()); - } - if (incoming.getMaxLength() != null) { - merged.setMaxLength(incoming.getMaxLength()); - } - if (incoming.getMinLength() != null) { - merged.setMinLength(incoming.getMinLength()); - } - if (incoming.getMaximum() != null) { - merged.setMaximum(incoming.getMaximum()); - } - if (incoming.getMinimum() != null) { - merged.setMinimum(incoming.getMinimum()); - } - if (incoming.getExclusiveMaximum() != null) { - merged.setExclusiveMaximum(incoming.getExclusiveMaximum()); - } - if (incoming.getExclusiveMinimum() != null) { - merged.setExclusiveMinimum(incoming.getExclusiveMinimum()); - } - if (incoming.getExclusiveMaximumValue() != null) { - merged.setExclusiveMaximumValue(incoming.getExclusiveMaximumValue()); - } - if (incoming.getExclusiveMinimumValue() != null) { - merged.setExclusiveMinimumValue(incoming.getExclusiveMinimumValue()); - } - if (incoming.getMultipleOf() != null) { - merged.setMultipleOf(incoming.getMultipleOf()); - } - if (incoming.getMaxItems() != null) { - merged.setMaxItems(incoming.getMaxItems()); - } - if (incoming.getMinItems() != null) { - merged.setMinItems(incoming.getMinItems()); - } - if (incoming.getUniqueItems() != null) { - merged.setUniqueItems(incoming.getUniqueItems()); - } - if (incoming.getMaxProperties() != null) { - merged.setMaxProperties(incoming.getMaxProperties()); - } - if (incoming.getMinProperties() != null) { - merged.setMinProperties(incoming.getMinProperties()); - } - if (incoming.getExtensions() != null) { - incoming.getExtensions().forEach((k, v) -> merged.addExtension(String.valueOf(k), v)); - } + ModelUtils.copyConstraints(incoming, merged); targetProperties.put(name, merged); } else { targetProperties.put(name, incoming); } } - /** - * True when the schema defines no type of its own: no type, no $ref, no items, - * no properties, no composition and no enum. - */ - private static boolean isConstraintOnlySchema(Schema schema) { - return ModelUtils.isAnyType(schema) - && schema.getItems() == null - && schema.getProperties() == null - && schema.getAllOf() == null - && schema.getOneOf() == null - && schema.getAnyOf() == null - && schema.getEnum() == null; - } - /** * Camelize the method name of the getter and setter * diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java index 17ce0a391b46..079679d4a4ca 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java @@ -2807,6 +2807,48 @@ public static void copyMetadata(Schema from, Schema to) { } } + /** + * Copies metadata plus the validation and format keywords that copyMetadata does not cover. + * Used when an allOf part only constrains an inherited property (see issue #4128), so no + * declared keyword is lost while the type is kept. + * + * @param from schema to copy from + * @param to schema to copy to + */ + public static void copyConstraints(Schema from, Schema to) { + copyMetadata(from, to); + if (from.getFormat() != null) { + to.setFormat(from.getFormat()); + } + if (from.getPattern() != null) { + to.setPattern(from.getPattern()); + } + if (from.getExclusiveMaximum() != null) { + to.setExclusiveMaximum(from.getExclusiveMaximum()); + } + if (from.getExclusiveMinimum() != null) { + to.setExclusiveMinimum(from.getExclusiveMinimum()); + } + if (from.getExclusiveMaximumValue() != null) { + to.setExclusiveMaximumValue(from.getExclusiveMaximumValue()); + } + if (from.getExclusiveMinimumValue() != null) { + to.setExclusiveMinimumValue(from.getExclusiveMinimumValue()); + } + if (from.getMultipleOf() != null) { + to.setMultipleOf(from.getMultipleOf()); + } + if (from.getUniqueItems() != null) { + to.setUniqueItems(from.getUniqueItems()); + } + if (from.getMaxProperties() != null) { + to.setMaxProperties(from.getMaxProperties()); + } + if (from.getMinProperties() != null) { + to.setMinProperties(from.getMinProperties()); + } + } + /** * Returns true if a schema is only metadata and not an actual type. * For example, a schema that only has a `description` without any `properties` or `$ref` defined. From 49352ca65439a46cce0d14128e3f8c16834c819f Mon Sep 17 00:00:00 2001 From: Subham Ashok Date: Tue, 4 Aug 2026 19:00:19 +0530 Subject: [PATCH 4/4] merge extensions per key and exclude const/not overlays from the merge Review feedback. Extensions from both parts now survive, overlay wins per key. Overlays carrying const or not keep the old replace behavior. Co-Authored-By: Claude Opus 4.7 --- .../main/java/org/openapitools/codegen/DefaultCodegen.java | 3 ++- .../java/org/openapitools/codegen/utils/ModelUtils.java | 7 +++++++ .../java/org/openapitools/codegen/DefaultCodegenTest.java | 3 +++ .../resources/3_0/allOf-nullable-typeless-override.yaml | 2 ++ 4 files changed, 14 insertions(+), 1 deletion(-) 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 2b0bdfaf0b60..bffbed2d6f6a 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 @@ -3665,7 +3665,8 @@ private void putProperty(Map targetProperties, String name, Sche Schema existing = targetProperties.get(name); if (existing != null && incoming != null && !ModelUtils.isAnyType(existing) - && ModelUtils.isMetadataOnlySchema(incoming) && incoming.getEnum() == null) { + && ModelUtils.isMetadataOnlySchema(incoming) + && incoming.getEnum() == null && incoming.getConst() == null && incoming.getNot() == null) { Schema merged = ModelUtils.cloneSchema(existing, specVersionGreaterThanOrEqualTo310(openAPI)); ModelUtils.copyConstraints(incoming, merged); targetProperties.put(name, merged); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java index 079679d4a4ca..704b3f8f6dab 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java @@ -2816,7 +2816,14 @@ public static void copyMetadata(Schema from, Schema to) { * @param to schema to copy to */ public static void copyConstraints(Schema from, Schema to) { + Map targetExtensions = to.getExtensions() == null ? null : new HashMap<>(to.getExtensions()); copyMetadata(from, to); + // merge extensions per key instead of replacing, the source wins on conflicts + if (targetExtensions != null && from.getExtensions() != null) { + Map mergedExtensions = new HashMap<>(targetExtensions); + mergedExtensions.putAll(from.getExtensions()); + to.setExtensions(mergedExtensions); + } if (from.getFormat() != null) { to.setFormat(from.getFormat()); } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java index 0ed1011f1c0d..28573bc2d2cf 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java @@ -1330,6 +1330,9 @@ public void testAllOfNullableWithoutTypeKeepsType() { assertEquals("String", addressId.dataType); assertTrue(addressId.isNullable); assertEquals(Integer.valueOf(36), addressId.maxLength); + // extensions from both parts survive the merge + assertEquals("keep", addressId.vendorExtensions.get("x-base-marker")); + assertEquals("added", addressId.vendorExtensions.get("x-overlay-marker")); } @Test diff --git a/modules/openapi-generator/src/test/resources/3_0/allOf-nullable-typeless-override.yaml b/modules/openapi-generator/src/test/resources/3_0/allOf-nullable-typeless-override.yaml index 9a40272bd4c6..32f2c9840914 100644 --- a/modules/openapi-generator/src/test/resources/3_0/allOf-nullable-typeless-override.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/allOf-nullable-typeless-override.yaml @@ -27,6 +27,7 @@ components: properties: addressId: type: string + x-base-marker: keep UpdateFirm: allOf: - $ref: "#/components/schemas/FirmProperties" @@ -37,3 +38,4 @@ components: addressId: nullable: true maxLength: 36 + x-overlay-marker: added