From 258b36b1c977aa974018fd833a3cbfad35a251ba Mon Sep 17 00:00:00 2001 From: Abhishek Chauhan <60182103+abhu85@users.noreply.github.com> Date: Fri, 20 Feb 2026 09:48:25 +0000 Subject: [PATCH 1/3] fix(dynamodb-enhanced): Fix NPE in ConverterUtils and EnhancedType for null values and wildcard types - Add null checks to ConverterUtils.validateDouble() and validateFloat() to prevent NPE when input is null - Add null check for rawClass in EnhancedType.hashCode() and equals() to support wildcard types (List) - Add comprehensive unit tests for null handling Fixes #6639 Fixes #5890 Co-Authored-By: Claude Opus 4.6 --- ...mazonDynamoDBEnhancedClient-npe-fixes.json | 6 ++ .../enhanced/dynamodb/EnhancedType.java | 4 +- .../internal/converter/ConverterUtils.java | 12 ++- .../enhanced/dynamodb/EnhancedTypeTest.java | 25 ++++++ .../converter/ConverterUtilsTest.java | 90 +++++++++++++++++++ 5 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 .changes/next-release/bugfix-AmazonDynamoDBEnhancedClient-npe-fixes.json create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtilsTest.java diff --git a/.changes/next-release/bugfix-AmazonDynamoDBEnhancedClient-npe-fixes.json b/.changes/next-release/bugfix-AmazonDynamoDBEnhancedClient-npe-fixes.json new file mode 100644 index 000000000000..42d1d2a109d0 --- /dev/null +++ b/.changes/next-release/bugfix-AmazonDynamoDBEnhancedClient-npe-fixes.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "Amazon DynamoDB Enhanced Client", + "contributor": "", + "description": "Fix NullPointerException in `ConverterUtils.validateDouble` and `ConverterUtils.validateFloat` when input is null, and in `EnhancedType.hashCode()` when using wildcard types." +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedType.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedType.java index e220854247b7..c63fd935bbe1 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedType.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedType.java @@ -566,7 +566,7 @@ public boolean equals(Object o) { if (isWildcard != enhancedType.isWildcard) { return false; } - if (!rawClass.equals(enhancedType.rawClass)) { + if (rawClass != null ? !rawClass.equals(enhancedType.rawClass) : enhancedType.rawClass != null) { return false; } if (rawClassParameters != null ? !rawClassParameters.equals(enhancedType.rawClassParameters) : @@ -584,7 +584,7 @@ public boolean equals(Object o) { @Override public int hashCode() { int result = (isWildcard ? 1 : 0); - result = 31 * result + rawClass.hashCode(); + result = 31 * result + (rawClass != null ? rawClass.hashCode() : 0); result = 31 * result + (rawClassParameters != null ? rawClassParameters.hashCode() : 0); result = 31 * result + (tableSchema != null ? tableSchema.hashCode() : 0); result = 31 * result + (documentConfiguration != null ? documentConfiguration.hashCode() : 0); diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtils.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtils.java index 0f6f20583187..ee1f001641f5 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtils.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtils.java @@ -36,18 +36,24 @@ private ConverterUtils() { /** * Validates that a given Double input is a valid double supported by {@link DoubleAttributeConverter}. - * @param input + * @param input the Double value to validate, may be null */ public static void validateDouble(Double input) { + if (input == null) { + return; + } Validate.isTrue(!Double.isNaN(input), "NaN is not supported by the default converters."); Validate.isTrue(Double.isFinite(input), "Infinite numbers are not supported by the default converters."); } /** - * Validates that a given Float input is a valid double supported by {@link FloatAttributeConverter}. - * @param input + * Validates that a given Float input is a valid float supported by {@link FloatAttributeConverter}. + * @param input the Float value to validate, may be null */ public static void validateFloat(Float input) { + if (input == null) { + return; + } Validate.isTrue(!Float.isNaN(input), "NaN is not supported by the default converters."); Validate.isTrue(Float.isFinite(input), "Infinite numbers are not supported by the default converters."); } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeTest.java index a8732a15aba4..16018896d796 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeTest.java @@ -239,6 +239,31 @@ public void documentOf_withEnhancedTypeConfiguration() { assertThat(type.documentConfiguration().get().preserveEmptyObject()).isTrue(); } + @Test + public void wildcardType_hashCode_doesNotRaiseNPE() { + // When using a wildcard type like List, the type parameter is a wildcard + // with rawClass = null. hashCode() should handle this without NPE. + EnhancedType> listWithWildcard = new EnhancedType>(){}; + EnhancedType wildcardParam = listWithWildcard.rawClassParameters().get(0); + + // This should not throw NPE + assertThatCode(() -> wildcardParam.hashCode()).doesNotThrowAnyException(); + } + + @Test + public void wildcardType_equals_handlesNullRawClass() { + // Wildcard types should be comparable via equals without NPE + EnhancedType> listWithWildcard1 = new EnhancedType>(){}; + EnhancedType> listWithWildcard2 = new EnhancedType>(){}; + + EnhancedType wildcard1 = listWithWildcard1.rawClassParameters().get(0); + EnhancedType wildcard2 = listWithWildcard2.rawClassParameters().get(0); + + // Wildcards should be equal to each other + assertThat(wildcard1).isEqualTo(wildcard2); + assertThat(wildcard1.hashCode()).isEqualTo(wildcard2.hashCode()); + } + public class InnerType { } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtilsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtilsTest.java new file mode 100644 index 000000000000..c50f4877fbb7 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtilsTest.java @@ -0,0 +1,90 @@ +/* + * 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.internal.converter; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class ConverterUtilsTest { + + @Test + void validateDouble_withNull_doesNotThrowException() { + assertThatCode(() -> ConverterUtils.validateDouble(null)) + .doesNotThrowAnyException(); + } + + @Test + void validateDouble_withValidValue_doesNotThrowException() { + assertThatCode(() -> ConverterUtils.validateDouble(1.5)) + .doesNotThrowAnyException(); + } + + @Test + void validateDouble_withNaN_throwsException() { + assertThatThrownBy(() -> ConverterUtils.validateDouble(Double.NaN)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("NaN is not supported"); + } + + @Test + void validateDouble_withPositiveInfinity_throwsException() { + assertThatThrownBy(() -> ConverterUtils.validateDouble(Double.POSITIVE_INFINITY)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Infinite numbers are not supported"); + } + + @Test + void validateDouble_withNegativeInfinity_throwsException() { + assertThatThrownBy(() -> ConverterUtils.validateDouble(Double.NEGATIVE_INFINITY)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Infinite numbers are not supported"); + } + + @Test + void validateFloat_withNull_doesNotThrowException() { + assertThatCode(() -> ConverterUtils.validateFloat(null)) + .doesNotThrowAnyException(); + } + + @Test + void validateFloat_withValidValue_doesNotThrowException() { + assertThatCode(() -> ConverterUtils.validateFloat(1.5f)) + .doesNotThrowAnyException(); + } + + @Test + void validateFloat_withNaN_throwsException() { + assertThatThrownBy(() -> ConverterUtils.validateFloat(Float.NaN)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("NaN is not supported"); + } + + @Test + void validateFloat_withPositiveInfinity_throwsException() { + assertThatThrownBy(() -> ConverterUtils.validateFloat(Float.POSITIVE_INFINITY)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Infinite numbers are not supported"); + } + + @Test + void validateFloat_withNegativeInfinity_throwsException() { + assertThatThrownBy(() -> ConverterUtils.validateFloat(Float.NEGATIVE_INFINITY)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Infinite numbers are not supported"); + } +} From 6537fdab79e53dd887964862978625be41641ccb Mon Sep 17 00:00:00 2001 From: abhu85 Date: Thu, 5 Mar 2026 12:47:46 +0000 Subject: [PATCH 2/3] fix(dynamodb-enhanced): Add toString() NPE fix, split out ConverterUtils changes - Add null check in innerToString() for wildcard types (rawClass is null) - Add test for toString() with wildcard types - Remove ConverterUtils changes (to be submitted as separate PR per maintainer request) - Update changelog to reflect EnhancedType-only changes --- ...mazonDynamoDBEnhancedClient-npe-fixes.json | 2 +- .../enhanced/dynamodb/EnhancedType.java | 15 ++-- .../internal/converter/ConverterUtils.java | 12 +-- .../enhanced/dynamodb/EnhancedTypeTest.java | 14 +++ .../converter/ConverterUtilsTest.java | 90 ------------------- 5 files changed, 27 insertions(+), 106 deletions(-) delete mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtilsTest.java diff --git a/.changes/next-release/bugfix-AmazonDynamoDBEnhancedClient-npe-fixes.json b/.changes/next-release/bugfix-AmazonDynamoDBEnhancedClient-npe-fixes.json index 42d1d2a109d0..88ac605c53ac 100644 --- a/.changes/next-release/bugfix-AmazonDynamoDBEnhancedClient-npe-fixes.json +++ b/.changes/next-release/bugfix-AmazonDynamoDBEnhancedClient-npe-fixes.json @@ -2,5 +2,5 @@ "type": "bugfix", "category": "Amazon DynamoDB Enhanced Client", "contributor": "", - "description": "Fix NullPointerException in `ConverterUtils.validateDouble` and `ConverterUtils.validateFloat` when input is null, and in `EnhancedType.hashCode()` when using wildcard types." + "description": "Fix NullPointerException in `EnhancedType.hashCode()`, `EnhancedType.equals()`, and `EnhancedType.toString()` when using wildcard types." } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedType.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedType.java index c63fd935bbe1..478bed5f4917 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedType.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedType.java @@ -541,14 +541,17 @@ private List> loadTypeParameters(Type type) { private StringBuilder innerToString() { StringBuilder result = new StringBuilder(); - result.append(rawClass.getTypeName()); + if (isWildcard) { + result.append("?"); + } else { + result.append(rawClass.getTypeName()); - if (null != rawClassParameters && !rawClassParameters.isEmpty()) { - result.append("<"); - result.append(rawClassParameters.stream().map(EnhancedType::innerToString).collect(Collectors.joining(", "))); - result.append(">"); + if (null != rawClassParameters && !rawClassParameters.isEmpty()) { + result.append("<"); + result.append(rawClassParameters.stream().map(EnhancedType::innerToString).collect(Collectors.joining(", "))); + result.append(">"); + } } - return result; } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtils.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtils.java index ee1f001641f5..0f6f20583187 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtils.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtils.java @@ -36,24 +36,18 @@ private ConverterUtils() { /** * Validates that a given Double input is a valid double supported by {@link DoubleAttributeConverter}. - * @param input the Double value to validate, may be null + * @param input */ public static void validateDouble(Double input) { - if (input == null) { - return; - } Validate.isTrue(!Double.isNaN(input), "NaN is not supported by the default converters."); Validate.isTrue(Double.isFinite(input), "Infinite numbers are not supported by the default converters."); } /** - * Validates that a given Float input is a valid float supported by {@link FloatAttributeConverter}. - * @param input the Float value to validate, may be null + * Validates that a given Float input is a valid double supported by {@link FloatAttributeConverter}. + * @param input */ public static void validateFloat(Float input) { - if (input == null) { - return; - } Validate.isTrue(!Float.isNaN(input), "NaN is not supported by the default converters."); Validate.isTrue(Float.isFinite(input), "Infinite numbers are not supported by the default converters."); } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeTest.java index 16018896d796..6d91bc1885f1 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeTest.java @@ -264,6 +264,20 @@ public void wildcardType_equals_handlesNullRawClass() { assertThat(wildcard1.hashCode()).isEqualTo(wildcard2.hashCode()); } + @Test + public void wildcardType_toString_doesNotRaiseNPE() { + // Wildcard types should be convertible to string without NPE + EnhancedType> listWithWildcard = new EnhancedType>(){}; + EnhancedType wildcardParam = listWithWildcard.rawClassParameters().get(0); + + // This should not throw NPE and should return "?" for wildcard + assertThatCode(() -> wildcardParam.toString()).doesNotThrowAnyException(); + assertThat(wildcardParam.toString()).isEqualTo("EnhancedType(?)"); + + // Also verify that the parent type renders correctly with wildcard + assertThat(listWithWildcard.toString()).isEqualTo("EnhancedType(java.util.List)"); + } + public class InnerType { } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtilsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtilsTest.java deleted file mode 100644 index c50f4877fbb7..000000000000 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtilsTest.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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.internal.converter; - -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import org.junit.jupiter.api.Test; - -class ConverterUtilsTest { - - @Test - void validateDouble_withNull_doesNotThrowException() { - assertThatCode(() -> ConverterUtils.validateDouble(null)) - .doesNotThrowAnyException(); - } - - @Test - void validateDouble_withValidValue_doesNotThrowException() { - assertThatCode(() -> ConverterUtils.validateDouble(1.5)) - .doesNotThrowAnyException(); - } - - @Test - void validateDouble_withNaN_throwsException() { - assertThatThrownBy(() -> ConverterUtils.validateDouble(Double.NaN)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("NaN is not supported"); - } - - @Test - void validateDouble_withPositiveInfinity_throwsException() { - assertThatThrownBy(() -> ConverterUtils.validateDouble(Double.POSITIVE_INFINITY)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Infinite numbers are not supported"); - } - - @Test - void validateDouble_withNegativeInfinity_throwsException() { - assertThatThrownBy(() -> ConverterUtils.validateDouble(Double.NEGATIVE_INFINITY)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Infinite numbers are not supported"); - } - - @Test - void validateFloat_withNull_doesNotThrowException() { - assertThatCode(() -> ConverterUtils.validateFloat(null)) - .doesNotThrowAnyException(); - } - - @Test - void validateFloat_withValidValue_doesNotThrowException() { - assertThatCode(() -> ConverterUtils.validateFloat(1.5f)) - .doesNotThrowAnyException(); - } - - @Test - void validateFloat_withNaN_throwsException() { - assertThatThrownBy(() -> ConverterUtils.validateFloat(Float.NaN)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("NaN is not supported"); - } - - @Test - void validateFloat_withPositiveInfinity_throwsException() { - assertThatThrownBy(() -> ConverterUtils.validateFloat(Float.POSITIVE_INFINITY)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Infinite numbers are not supported"); - } - - @Test - void validateFloat_withNegativeInfinity_throwsException() { - assertThatThrownBy(() -> ConverterUtils.validateFloat(Float.NEGATIVE_INFINITY)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Infinite numbers are not supported"); - } -} From 18e29d1b1b34251108883d4f2e200541ca37ae0c Mon Sep 17 00:00:00 2001 From: Abhishek Chauhan Date: Mon, 16 Mar 2026 16:45:49 -0500 Subject: [PATCH 3/3] test: add test for wildcard vs non-wildcard equality comparison Addresses review feedback to cover the ternary logic in equals() when comparing a wildcard type (rawClass=null) against a non-wildcard type (rawClass!=null). Tests both comparison directions. --- .../awssdk/enhanced/dynamodb/EnhancedTypeTest.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeTest.java index 6d91bc1885f1..35d73a88ac9c 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/EnhancedTypeTest.java @@ -264,6 +264,18 @@ public void wildcardType_equals_handlesNullRawClass() { assertThat(wildcard1.hashCode()).isEqualTo(wildcard2.hashCode()); } + @Test + public void wildcardType_equals_notEqualToNonWildcard() { + // A wildcard type (rawClass=null) should not be equal to a non-wildcard type (rawClass!=null) + EnhancedType> listWithWildcard = new EnhancedType>(){}; + EnhancedType wildcardType = listWithWildcard.rawClassParameters().get(0); + EnhancedType nonWildcardType = EnhancedType.of(String.class); + + // Wildcard vs non-wildcard should not be equal (tests both comparison directions) + assertThat(wildcardType).isNotEqualTo(nonWildcardType); + assertThat(nonWildcardType).isNotEqualTo(wildcardType); + } + @Test public void wildcardType_toString_doesNotRaiseNPE() { // Wildcard types should be convertible to string without NPE