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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "bugfix",
"category": "Amazon DynamoDB Enhanced Client",
"contributor": "",
"description": "Fixed DynamoDbEnhancedClient TableSchema::itemToMap to return a map that contains a consistent representation of null top-level (non-flattened) attributes and flattened attributes when their enclosing member is null and ignoreNulls is set to false."
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ private B mapToItem(B thisBuilder,
private Map<String, AttributeValue> itemToMap(T item, boolean ignoreNulls) {
T1 otherItem = this.otherItemGetter.apply(item);

if (otherItem == null) {
if (otherItem == null && ignoreNulls) {
return Collections.emptyMap();
}

Expand Down Expand Up @@ -612,15 +612,24 @@ public Map<String, AttributeValue> itemToMap(T item, boolean ignoreNulls) {

attributeMappers.forEach(attributeMapper -> {
String attributeKey = attributeMapper.attributeName();
AttributeValue attributeValue = attributeMapper.attributeGetterMethod().apply(item);
AttributeValue attributeValue = item == null ?
AttributeValue.fromNul(true) :
attributeMapper.attributeGetterMethod().apply(item);

if (!ignoreNulls || !isNullAttributeValue(attributeValue)) {
attributeValueMap.put(attributeKey, attributeValue);
}
});

Set<FlattenedMapper<T, B, ?>> processedMappers = new LinkedHashSet<>();
flattenedObjectMappers.forEach((name, flattenedMapper) -> {
attributeValueMap.putAll(flattenedMapper.itemToMap(item, ignoreNulls));
if (item != null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also when item != null, the same flattenedMapper.itemToMap() is called multiple times for the same flattened object. Is my understanding correct?

Can we add this so it loops just once for every flattenMapper?

if (processedMappers.add(flattenedMapper)) {
            attributeValueMap.putAll(flattenedMapper.itemToMap(item, ignoreNulls));
        }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I updated the PR with this improvement.

if(processedMappers.add(flattenedMapper)) {
attributeValueMap.putAll(flattenedMapper.itemToMap(item, ignoreNulls));
}
} else if (!ignoreNulls) {
attributeValueMap.put(name, AttributeValue.fromNul(true));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the right behavior ? Here name is an attribute name from the flattened object, not the flattened field name itself.

Copy link
Contributor Author

@iulianbudau iulianbudau Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the behavior is correct. The whole idea with the flattened is to bring the attributes from all the flattened members to root in the returned map, as like they belong to the parent root level. The flattened fields themself containing those attributes will actually not be visible in the DynamoDb table.

}
});

if (flattenedMapMapper != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
import software.amazon.awssdk.enhanced.dynamodb.ExecutionContext;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractImmutable;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractImmutable;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractNestedImmutable;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AttributeConverterBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AttributeConverterNoConstructorBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.CommonTypesBean;
Expand All @@ -62,7 +63,10 @@
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.EnumBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.ExtendedBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedBeanBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedFirstNestedBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedFirstNestedBean.FlattenedSecondNestedBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedImmutableBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedNestedImmutableBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FluentSetterBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.IgnoredAttributeBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.InvalidBean;
Expand Down Expand Up @@ -276,6 +280,128 @@ public void dynamoDbFlatten_correctlyFlattensImmutableAttributes() {
assertThat(itemMap, hasEntry("attribute2", stringValue("two")));
}

@Test
public void dynamoDbFlatten_correctlyFlattensNullImmutableAttributes() {
BeanTableSchema<FlattenedImmutableBean> beanTableSchema = BeanTableSchema.create(FlattenedImmutableBean.class);
AbstractImmutable abstractImmutable = AbstractImmutable.builder().build();
FlattenedImmutableBean flattenedImmutableBean = new FlattenedImmutableBean();
flattenedImmutableBean.setId("id-value");
flattenedImmutableBean.setAbstractImmutable(abstractImmutable);

Map<String, AttributeValue> itemMap = beanTableSchema.itemToMap(flattenedImmutableBean, false);
assertThat(itemMap.size(), is(3));
assertThat(itemMap, hasEntry("id", stringValue("id-value")));
assertThat(itemMap, hasEntry("attribute1", AttributeValue.fromNul(true)));
assertThat(itemMap, hasEntry("attribute2", AttributeValue.fromNul(true)));
}

@Test
public void dynamoDbFlatten_correctlyFlattensNestedImmutableAttributes() {
BeanTableSchema<FlattenedNestedImmutableBean> beanTableSchema =
BeanTableSchema.create(FlattenedNestedImmutableBean.class);
AbstractNestedImmutable abstractNestedImmutable2 =
AbstractNestedImmutable.builder().attribute2("nested-two").build();
AbstractNestedImmutable abstractNestedImmutable1 =
AbstractNestedImmutable.builder().attribute2("two").abstractNestedImmutableOne(abstractNestedImmutable2).build();
FlattenedNestedImmutableBean flattenedNestedImmutableBean = new FlattenedNestedImmutableBean();
flattenedNestedImmutableBean.setId("id-value");
flattenedNestedImmutableBean.setAttribute1("one");
flattenedNestedImmutableBean.setAbstractNestedImmutable(abstractNestedImmutable1);

Map<String, AttributeValue> nestedAttributesMap = new HashMap<>();
nestedAttributesMap.put("abstractNestedImmutableOne", AttributeValue.fromNul(true));
nestedAttributesMap.put("attribute2", stringValue("nested-two"));

AttributeValue expectedNestedAttribute =
AttributeValue.builder().m(Collections.unmodifiableMap(nestedAttributesMap)).build();

Map<String, AttributeValue> itemMap = beanTableSchema.itemToMap(flattenedNestedImmutableBean, false);
assertThat(itemMap.size(), is(4));
assertThat(itemMap, hasEntry("id", stringValue("id-value")));
assertThat(itemMap, hasEntry("attribute1", stringValue("one")));
assertThat(itemMap, hasEntry("attribute2", stringValue("two")));
assertThat(itemMap, hasEntry("abstractNestedImmutableOne", expectedNestedAttribute));
}

@Test
public void dynamoDbFlatten_correctlyFlattensNullNestedImmutableAttributes() {
BeanTableSchema<FlattenedNestedImmutableBean> beanTableSchema =
BeanTableSchema.create(FlattenedNestedImmutableBean.class);
AbstractNestedImmutable abstractNestedImmutable = AbstractNestedImmutable.builder().build();
FlattenedNestedImmutableBean flattenedNestedImmutableBean = new FlattenedNestedImmutableBean();
flattenedNestedImmutableBean.setId("id-value");
flattenedNestedImmutableBean.setAttribute1("one");
flattenedNestedImmutableBean.setAbstractNestedImmutable(abstractNestedImmutable);

Map<String, AttributeValue> itemMap = beanTableSchema.itemToMap(flattenedNestedImmutableBean, false);
assertThat(itemMap.size(), is(4));
assertThat(itemMap, hasEntry("id", stringValue("id-value")));
assertThat(itemMap, hasEntry("attribute1", stringValue("one")));
assertThat(itemMap, hasEntry("attribute2", AttributeValue.fromNul(true)));
assertThat(itemMap, hasEntry("abstractNestedImmutableOne", AttributeValue.fromNul(true)));
}

@Test
public void dynamoDbFlatten_correctlyFlattensSecondNullNestedAttributes_IgnoreNullsFalse() {
BeanTableSchema<FlattenedFirstNestedBean> beanTableSchema =
BeanTableSchema.create(FlattenedFirstNestedBean.class);
FlattenedFirstNestedBean flattenedFirstNestedBean = new FlattenedFirstNestedBean();
flattenedFirstNestedBean.setId("id-value");

Map<String, AttributeValue> itemMap = beanTableSchema.itemToMap(flattenedFirstNestedBean, false);
assertThat(itemMap.size(), is(4));
assertThat(itemMap, hasEntry("id", stringValue("id-value")));
assertThat(itemMap, hasEntry("secondId", AttributeValue.fromNul(true)));
assertThat(itemMap, hasEntry("thirdId", AttributeValue.fromNul(true)));
assertThat(itemMap, hasEntry("flattenedFourthBean", AttributeValue.fromNul(true)));
}

@Test
public void dynamoDbFlatten_correctlyFlattensSecondNullNestedAttributes_IgnoreNullsTrue() {
BeanTableSchema<FlattenedFirstNestedBean> beanTableSchema =
BeanTableSchema.create(FlattenedFirstNestedBean.class);
FlattenedFirstNestedBean flattenedFirstNestedBean = new FlattenedFirstNestedBean();
flattenedFirstNestedBean.setId("id-value");

Map<String, AttributeValue> itemMap = beanTableSchema.itemToMap(flattenedFirstNestedBean, true);
assertThat(itemMap.size(), is(1));
assertThat(itemMap, hasEntry("id", stringValue("id-value")));
}

@Test
public void dynamoDbFlatten_correctlyFlattensThirdNullNestedAttributes_IgnoreNullsFalse() {
BeanTableSchema<FlattenedFirstNestedBean> beanTableSchema =
BeanTableSchema.create(FlattenedFirstNestedBean.class);
FlattenedSecondNestedBean flattenedSecondNestedBean = new FlattenedSecondNestedBean();
flattenedSecondNestedBean.setSecondId("second-id-value");
FlattenedFirstNestedBean flattenedFirstNestedBean = new FlattenedFirstNestedBean();
flattenedFirstNestedBean.setId("id-value");
flattenedFirstNestedBean.setFlattenedSecondNestedBean(flattenedSecondNestedBean);

Map<String, AttributeValue> itemMap = beanTableSchema.itemToMap(flattenedFirstNestedBean, false);
assertThat(itemMap.size(), is(4));
assertThat(itemMap, hasEntry("id", stringValue("id-value")));
assertThat(itemMap, hasEntry("secondId", stringValue("second-id-value")));
assertThat(itemMap, hasEntry("thirdId", AttributeValue.fromNul(true)));
assertThat(itemMap, hasEntry("flattenedFourthBean", AttributeValue.fromNul(true)));
}

@Test
public void dynamoDbFlatten_correctlyFlattensThirdNullNestedAttributes_IgnoreNullsTrue() {
BeanTableSchema<FlattenedFirstNestedBean> beanTableSchema =
BeanTableSchema.create(FlattenedFirstNestedBean.class);
FlattenedSecondNestedBean flattenedSecondNestedBean = new FlattenedSecondNestedBean();
flattenedSecondNestedBean.setSecondId("second-id-value");
FlattenedFirstNestedBean flattenedFirstNestedBean = new FlattenedFirstNestedBean();
flattenedFirstNestedBean.setId("id-value");
flattenedFirstNestedBean.setFlattenedSecondNestedBean(flattenedSecondNestedBean);

Map<String, AttributeValue> itemMap = beanTableSchema.itemToMap(flattenedFirstNestedBean, true);
assertThat(itemMap.size(), is(2));
assertThat(itemMap, hasEntry("id", stringValue("id-value")));
assertThat(itemMap, hasEntry("secondId", stringValue("second-id-value")));
}

@Test
public void documentBean_correctlyMapsBeanAttributes() {
BeanTableSchema<DocumentBean> beanTableSchema = BeanTableSchema.create(DocumentBean.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItem;
import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemComposedClass;
import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithSort;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten;
import software.amazon.awssdk.enhanced.dynamodb.mapper.testimmutables.EntityEnvelopeImmutable;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;

Expand Down Expand Up @@ -782,6 +783,62 @@ public Consumer<StaticTableMetadata.Builder> modifyMetadata() {
}
}

public static final class FakeMappedItemWithDeep {
private String documentString;
private FakeDocumentWithDeep aFakeDocument;

public String getDocumentString() {
return documentString;
}

public void setDocumentString(String documentString) {
this.documentString = documentString;
}

@DynamoDbFlatten
public FakeDocumentWithDeep getAFakeDocument() {
return aFakeDocument;
}

public void setAFakeDocument(FakeDocumentWithDeep aFakeDocument) {
this.aFakeDocument = aFakeDocument;
}
}

public static final class FakeDocumentWithDeep {
private Integer documentInteger;
private DeepFakeDocument deepFakeDocument;

public Integer getDocumentInteger() {
return documentInteger;
}

public void setDocumentInteger(Integer documentInteger) {
this.documentInteger = documentInteger;
}

@DynamoDbFlatten
public DeepFakeDocument getDeepFakeDocument() {
return deepFakeDocument;
}

public void setDeepFakeDocument(DeepFakeDocument deepFakeDocument) {
this.deepFakeDocument = deepFakeDocument;
}
}

public static final class DeepFakeDocument {
private String deepString;

public String getDeepString() {
return deepString;
}

public void setDeepString(String deepString) {
this.deepString = deepString;
}
}

@Mock
private AttributeConverterProvider provider1;

Expand Down Expand Up @@ -1388,6 +1445,67 @@ public void buildAbstractWithFlatten() {
is(singletonMap("documentString", AttributeValue.builder().s("test-string").build())));
}

@Test
public void buildAbstractWithFlattenAndIgnoreNullAsFalse() {
StaticTableSchema<FakeMappedItem> tableSchema =
StaticTableSchema.builder(FakeMappedItem.class)
.flatten(FAKE_DOCUMENT_TABLE_SCHEMA,
FakeMappedItem::getAFakeDocument,
FakeMappedItem::setAFakeDocument)
.build();

FakeDocument document = FakeDocument.of("test-string", null);
FakeMappedItem item = FakeMappedItem.builder().aFakeDocument(document).build();

Map<String, AttributeValue> attributeMapWithNulls = tableSchema.itemToMap(item, false);
assertThat(attributeMapWithNulls.size(), is(2));
assertThat(attributeMapWithNulls, hasEntry("documentString", AttributeValue.builder().s("test-string").build()));
assertThat(attributeMapWithNulls, hasEntry("documentInteger", AttributeValue.fromNul(true)));
}

@Test
public void buildAbstractWithNestedFlattenAndIgnoreNullAsFalse() {
StaticTableSchema<DeepFakeDocument> deepSchema =
StaticTableSchema.builder(DeepFakeDocument.class)
.newItemSupplier(DeepFakeDocument::new)
.addAttribute(String.class, a -> a.name("deepString")
.getter(DeepFakeDocument::getDeepString)
.setter(DeepFakeDocument::setDeepString))
.build();

StaticTableSchema<FakeDocumentWithDeep> nestedSchema =
StaticTableSchema.builder(FakeDocumentWithDeep.class)
.newItemSupplier(FakeDocumentWithDeep::new)
.addAttribute(Integer.class, a -> a.name("documentInteger")
.getter(FakeDocumentWithDeep::getDocumentInteger)
.setter(FakeDocumentWithDeep::setDocumentInteger))
.flatten(deepSchema,
FakeDocumentWithDeep::getDeepFakeDocument,
FakeDocumentWithDeep::setDeepFakeDocument)
.build();

StaticTableSchema<FakeMappedItemWithDeep> tableSchema =
StaticTableSchema.builder(FakeMappedItemWithDeep.class)
.newItemSupplier(FakeMappedItemWithDeep::new)
.addAttribute(String.class, a -> a.name("documentString")
.getter(FakeMappedItemWithDeep::getDocumentString)
.setter(FakeMappedItemWithDeep::setDocumentString))
.flatten(nestedSchema,
FakeMappedItemWithDeep::getAFakeDocument,
FakeMappedItemWithDeep::setAFakeDocument)
.build();

FakeMappedItemWithDeep item = new FakeMappedItemWithDeep();
item.setDocumentString("top-level-string");

Map<String, AttributeValue> attributeMap = tableSchema.itemToMap(item, false);

assertThat(attributeMap.size(), is(3));
assertThat(attributeMap, hasEntry("documentString", AttributeValue.builder().s("top-level-string").build()));
assertThat(attributeMap, hasEntry("documentInteger", AttributeValue.fromNul(true)));
assertThat(attributeMap, hasEntry("deepString", AttributeValue.fromNul(true)));
}

@Test
public void buildAbstractExtends() {
StaticTableSchema<FakeAbstractSuperclass> superclassTableSchema =
Expand Down
Loading