From cf36aa0d836f75fc61a1b6b49d8433757ba11cad Mon Sep 17 00:00:00 2001 From: Saranya Somepalli Date: Mon, 13 Apr 2026 07:23:33 -0700 Subject: [PATCH] Fix NPE for null values in Map and List attribute converters --- ...-AmazonDynamoDBEnhancedClient-f887fc6.json | 6 ++ .../attribute/ListAttributeConverter.java | 5 +- .../attribute/MapAttributeConverter.java | 5 +- .../CollectionNullValueConverterTest.java | 98 +++++++++++++++++++ 4 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 .changes/next-release/bugfix-AmazonDynamoDBEnhancedClient-f887fc6.json create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/attribute/CollectionNullValueConverterTest.java diff --git a/.changes/next-release/bugfix-AmazonDynamoDBEnhancedClient-f887fc6.json b/.changes/next-release/bugfix-AmazonDynamoDBEnhancedClient-f887fc6.json new file mode 100644 index 000000000000..32948b1dd81a --- /dev/null +++ b/.changes/next-release/bugfix-AmazonDynamoDBEnhancedClient-f887fc6.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "Amazon DynamoDB Enhanced Client", + "contributor": "", + "description": "Fix NullPointerException when converting null values in `Map` and `List` attributes by handling nulls in `MapAttributeConverter` and `ListAttributeConverter` before delegating to element converters. Fixes [#6639](https://github.com/aws/aws-sdk-java-v2/issues/6639)" +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/ListAttributeConverter.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/ListAttributeConverter.java index eeda7e035621..a11835404d74 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/ListAttributeConverter.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/ListAttributeConverter.java @@ -30,6 +30,7 @@ import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues; import software.amazon.awssdk.enhanced.dynamodb.internal.converter.TypeConvertingVisitor; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; @@ -145,7 +146,9 @@ public AttributeValueType attributeValueType() { @Override public AttributeValue transformFrom(T input) { return EnhancedAttributeValue.fromListOfAttributeValues(input.stream() - .map(elementConverter::transformFrom) + .map(e -> e == null + ? AttributeValues.nullAttributeValue() + : elementConverter.transformFrom(e)) .collect(toList())) .toAttributeValue(); } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/MapAttributeConverter.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/MapAttributeConverter.java index edaa999ac98b..ef19cea362a2 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/MapAttributeConverter.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/MapAttributeConverter.java @@ -30,6 +30,7 @@ import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues; import software.amazon.awssdk.enhanced.dynamodb.internal.converter.StringConverter; import software.amazon.awssdk.enhanced.dynamodb.internal.converter.TypeConvertingVisitor; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; @@ -163,7 +164,9 @@ public EnhancedType type() { public EnhancedAttributeValue toAttributeValue(T input) { Map result = new LinkedHashMap<>(); - input.forEach((k, v) -> result.put(keyConverter.toString(k), valueConverter.transformFrom(v))); + input.forEach((k, v) -> result.put(keyConverter.toString(k), + v == null ? AttributeValues.nullAttributeValue() + : valueConverter.transformFrom(v))); return EnhancedAttributeValue.fromMap(result); } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/attribute/CollectionNullValueConverterTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/attribute/CollectionNullValueConverterTest.java new file mode 100644 index 000000000000..b739026f6116 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/converters/attribute/CollectionNullValueConverterTest.java @@ -0,0 +1,98 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.converters.attribute; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.BigDecimalAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.BooleanAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.DoubleAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.FloatAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.InstantAsStringAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.IntegerAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.ListAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.LongAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.MapAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.StringAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.internal.converter.string.StringStringConverter; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * Verifies that {@link MapAttributeConverter} and {@link ListAttributeConverter} correctly handle null + * values/elements by converting them to DynamoDB NULL type, regardless of the element converter type. + */ +public class CollectionNullValueConverterTest { + + private static final AttributeValue NULL_ATTR = AttributeValue.builder().nul(true).build(); + + private static Stream elementConverters() { + return Stream.of( + Arguments.of("String", StringAttributeConverter.create()), + Arguments.of("Boolean", BooleanAttributeConverter.create()), + Arguments.of("Integer", IntegerAttributeConverter.create()), + Arguments.of("Long", LongAttributeConverter.create()), + Arguments.of("Float", FloatAttributeConverter.create()), + Arguments.of("Double", DoubleAttributeConverter.create()), + Arguments.of("BigDecimal", BigDecimalAttributeConverter.create()), + Arguments.of("Instant", InstantAsStringAttributeConverter.create()) + ); + } + + @ParameterizedTest(name = "Map with null {0} value produces DynamoDB NULL") + @MethodSource("elementConverters") + void mapConverter_nullValue_producesNullAttributeValue(String name, AttributeConverter elementConverter) { + @SuppressWarnings("unchecked") + AttributeConverter converter = (AttributeConverter) elementConverter; + MapAttributeConverter> mapConverter = + MapAttributeConverter.mapConverter(StringStringConverter.create(), converter); + + Map input = new HashMap<>(); + input.put("key1", null); + + AttributeValue result = mapConverter.transformFrom(input); + + assertThat(result.hasM()).isTrue(); + assertThat(result.m().get("key1")).isEqualTo(NULL_ATTR); + } + + @ParameterizedTest(name = "List with null {0} element produces DynamoDB NULL") + @MethodSource("elementConverters") + void listConverter_nullElement_producesNullAttributeValue(String name, AttributeConverter elementConverter) { + @SuppressWarnings("unchecked") + AttributeConverter converter = (AttributeConverter) elementConverter; + ListAttributeConverter> listConverter = ListAttributeConverter.create(converter); + + List input = Arrays.asList(null, null); + + AttributeValue result = listConverter.transformFrom(input); + + assertThat(result.hasL()).isTrue(); + assertThat(result.l()).hasSize(2); + assertThat(result.l().get(0)).isEqualTo(NULL_ATTR); + assertThat(result.l().get(1)).isEqualTo(NULL_ATTR); + } +}