aws / aws-sdk-java-v2

The official AWS SDK for Java - Version 2
Apache License 2.0
2.16k stars 833 forks source link

Enhanced DynamoDB NPE on putItem with null map value #2282

Open xstex opened 3 years ago

xstex commented 3 years ago

A null pointer exception is hit in the dynamoDB enhanced client when performing a put of a map attribute with a null value. This happens only when using a beanschema, not a staticschema.

Describe the bug

public class SDKTest {

  public static final StaticTableSchema<Chapter> CHAPTER_SCHEMA = StaticTableSchema.builder(Chapter.class)
    .newItemSupplier(Chapter::new)
    .addAttribute(Integer.class, a -> a.name("page").getter(Chapter::getPage).setter(Chapter::setPage))
    .addAttribute(String.class, a -> a.name("text").getter(Chapter::getText).setter(Chapter::setText))
    .build();

  public static final StaticTableSchema<Book> BOOK_SCHEMA = StaticTableSchema.builder(Book.class)
    .newItemSupplier(Book::new)
    .addAttribute(String.class, a -> a.name("id").tags(primaryPartitionKey()).getter(Book::getId).setter(Book::setId))
    .addAttribute(EnhancedType.mapOf(EnhancedType.of(String.class), EnhancedType.documentOf(Chapter.class, CHAPTER_SCHEMA)),
      a -> a.name("map").getter(Book::getChapters).setter(Book::setChapters))
    .build();

  @DynamoDbBean
  public static class Book {
    private String id;
    private Map<String, Chapter> chapters;

    public Book() {}
    public Book(String id) { setId(id); }

    @DynamoDbPartitionKey public String getId() {  return id; }
    public void setId(String id) {  this.id = id; }

    public Map<String, Chapter> getChapters() { return chapters; }
    public void setChapters(Map<String, Chapter> chapters) {  this.chapters = chapters;  }
  }

  @DynamoDbBean
  public static class Chapter {
    private Integer page;
    private String text;

    public Integer getPage() {  return page;  }
    public void setPage(Integer page) {  this.page = page;  }

    public String getText() {  return text;  }
    public void setText(String text) {  this.text = text;  }
  }

  public void testStatic(DynamoDbClient client, DynamoDbEnhancedClient enhancedClient) throws Exception {
    DynamoDbTable<Book> table = enhancedClient.table("books", BOOK_SCHEMA);
    table.createTable();

    client.putItem(b -> b.tableName("books")
      .item(new HashMap<String, AttributeValue>() {{
        put("id", AttributeValue.builder().s("123").build());
        put("chapters", AttributeValue.builder()
          .m(Collections.singletonMap("First", AttributeValue.builder().nul(true).build()))
          .build());
      }}));

    Book item = table.getItem(new Book("123"));
    table.putItem(item);
  }

  public void testBean(DynamoDbClient client, DynamoDbEnhancedClient enhancedClient) throws Exception {
    DynamoDbTable<Book> table = enhancedClient.table("books", TableSchema.fromBean(Book.class));
    table.createTable();

    client.putItem(b -> b.tableName("books")
      .item(new HashMap<String, AttributeValue>() {{
        put("id", AttributeValue.builder().s("123").build());
        put("chapters", AttributeValue.builder()
          .m(Collections.singletonMap("First", AttributeValue.builder().nul(true).build()))
          .build());
      }}));

    Book item = table.getItem(new Book("123"));
    table.putItem(item); // <------------------------------LINE THAT THROWS EXCEPTION
  }
}

Behavior

testStatic works, while testBean throws NPE with the following trace:

java.lang.NullPointerException
    at software.amazon.awssdk.enhanced.dynamodb.internal.mapper.ResolvedImmutableAttribute.lambda$create$0(ResolvedImmutableAttribute.java:48)
    at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema.lambda$itemToMap$5(StaticImmutableTableSchema.java:491)
    at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema.itemToMap(StaticImmutableTableSchema.java:489)
    at software.amazon.awssdk.enhanced.dynamodb.mapper.WrappedTableSchema.itemToMap(WrappedTableSchema.java:59)
    at software.amazon.awssdk.enhanced.dynamodb.mapper.WrappedTableSchema.itemToMap(WrappedTableSchema.java:59)
    at software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.DocumentAttributeConverter.transformFrom(DocumentAttributeConverter.java:47)
    at software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.MapAttributeConverter$Delegate.lambda$toAttributeValue$0(MapAttributeConverter.java:165)
    at software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.MapAttributeConverter$Delegate.toAttributeValue(MapAttributeConverter.java:165)
    at software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.MapAttributeConverter.transformFrom(MapAttributeConverter.java:138)
    at software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.MapAttributeConverter.transformFrom(MapAttributeConverter.java:76)
    at software.amazon.awssdk.enhanced.dynamodb.internal.mapper.StaticAttributeType.objectToAttributeValue(StaticAttributeType.java:40)
    at software.amazon.awssdk.enhanced.dynamodb.internal.mapper.ResolvedImmutableAttribute.lambda$create$0(ResolvedImmutableAttribute.java:49)
    at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema.lambda$itemToMap$5(StaticImmutableTableSchema.java:491)
    at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema.itemToMap(StaticImmutableTableSchema.java:489)
    at software.amazon.awssdk.enhanced.dynamodb.mapper.WrappedTableSchema.itemToMap(WrappedTableSchema.java:59)
    at software.amazon.awssdk.enhanced.dynamodb.mapper.WrappedTableSchema.itemToMap(WrappedTableSchema.java:59)
    at software.amazon.awssdk.enhanced.dynamodb.internal.operations.PutItemOperation.generateRequest(PutItemOperation.java:71)
    at software.amazon.awssdk.enhanced.dynamodb.internal.operations.PutItemOperation.generateRequest(PutItemOperation.java:40)
    at software.amazon.awssdk.enhanced.dynamodb.internal.operations.CommonOperation.execute(CommonOperation.java:113)
    at software.amazon.awssdk.enhanced.dynamodb.internal.operations.TableOperation.executeOnPrimaryIndex(TableOperation.java:59)
    at software.amazon.awssdk.enhanced.dynamodb.internal.client.DefaultDynamoDbTable.putItem(DefaultDynamoDbTable.java:179)
    at software.amazon.awssdk.enhanced.dynamodb.internal.client.DefaultDynamoDbTable.putItem(DefaultDynamoDbTable.java:187)
    at software.amazon.awssdk.enhanced.dynamodb.internal.client.DefaultDynamoDbTable.putItem(DefaultDynamoDbTable.java:192)
    at com.magicbinder.core.SDKTest.testBean

Your Environment

debora-ito commented 3 years ago

@xstex thank you for reaching out. I've run your code sample and I'm seeing two issues:

  1. When using the StaticTableSchema, the Book object obtained after getItem() does not contain a chapters map. That's why calling the DynamoDB Enhanced client putItem() does not throw any exception, but the new added book item only contains id and the "First" -> null map is lost.
  2. When using the Bean schema, the Book obtained after getItem() contains both id and chapters as expected, but calling putItem() is throwing a null pointer exception.

I'm still investigating what can be the cause.

svenallers commented 3 years ago

We currently do have the same issue. Are there any updates on this topic? I think that I found sth. that could have caused the issue. When looking into the Delegate of the MapAttributeConverter, then it seems like that it is not considering that the value could be null in the toAttributeValue() function. I might be able to contribute a fix, but I probably won't find time to work on that topic before Tuesday

matios13 commented 6 months ago

Any progres on that bug?