David A Good

DynamoDB Enhanced Client for Java: Missing Setters Cause Misleading Error or Unexpected Behavior

December 01, 2020

Problem

When using the AWS SDK v2 for Java’s DynamoDB Enhanced Client, you’re getting one of these exceptions:

  • IllegalArgumentException: Attempt to execute an operation that requires a primary index without defining any primary key attributes in the table metadata
  • IllegalArgumentException: Attempt to execute an operation against an index that requires a partition key without assigning a partition key to that index. Index name: $PRIMARY_INDEX
  • IllegalArgumentException: A sort key value was supplied for an index that does not support one. Index: $PRIMARY_INDEX

Solution

Make sure every DynamoDB attribute on your entity has a publicly-accessible setter with the same name (case-sensitive) as the getter.

For example, if your entity has a userName attribute with a getter:

public String getUserName() { 
    return this.userName; 
}

…then it must also have a matching setter which is publicly accessible:

public void setUserName(String userName) { 
    this.userName = userName; 
}

Similarly, if you have a getter for a static sort key like this:

@DynamoDbSortKey
@DynamoDbAttribute("SK")
public String getSortKey() {
    return "A";
}

…you must have a corresponding, publicly-accessible setter like this:

public void setSortKey(String sortKey) {
    // Do nothing, this is a derived attribute
}

Details

In order for the AWS SDK v2 for Java’s Enhanced Client to detect an attribute on a @DynamoDbBean, the field must have a setter. In other words, just having a getter is not enough, even for derived attributes. This may or may not be obvious, but forgetting the setter for an attribute can result in a misleading error, or the attribute being silently ignored as we will see below.

For example, consider this DynamoDB bean:

import lombok.Data;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey;

@DynamoDbBean
@Data
public class Customer {

    private String id;

    @DynamoDbPartitionKey
    @DynamoDbAttribute("PK")
    public String getPartitionKey() {
        return "CUSTOMER#" + this.id;
    }

    @DynamoDbSortKey
    @DynamoDbAttribute("SK")
    public String getSortKey() {
        return "A";
    }

}

…and this sample application code:

import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable;

public class App {

    private final DynamoDbClient dynamoDbClient;
    private final DynamoDbEnhancedClient dynamoDbEnhancedClient;
    private final DynamoDbTable<Customer> customerTable;

    public static void main(String[] args) {
        App app = new App();
        Customer customer = new Customer("123", "ABC");
        app.saveCustomer(customer);
    }

    public App() {
        this.dynamoDbClient = DynamoDbClient.builder()
                .region(Region.US_EAST_1)
                .build();
        this.dynamoDbEnhancedClient = DynamoDbEnhancedClient.builder()
                .dynamoDbClient(dynamoDbClient)
                .build();
        this.customerTable = 
                dynamoDbEnhancedClient.table(
                        "MyTable", TableSchema.fromClass(Customer.class));
    }

    public void saveCustomer(Customer customer) {
       this.customerTable.putItem(customer);
    }

}

Running this code with fail with the following error:

java.lang.IllegalArgumentException: Attempt to execute an operation that requires a primary index without defining any primary key attributes in the table metadata.
	at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata.getIndex(StaticTableMetadata.java:141)
	at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata.indexPartitionKey(StaticTableMetadata.java:78)
	at software.amazon.awssdk.enhanced.dynamodb.TableMetadata.primaryPartitionKey(TableMetadata.java:121)
	at software.amazon.awssdk.enhanced.dynamodb.internal.operations.PutItemOperation.generateRequest(PutItemOperation.java:68)
	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 com.davidagood.dynamo.v2.App.saveCustomer(App.java:49)
	at com.davidagood.dynamo.v2.App.main(App.java:31)

If we inspect the TableSchema, we notice it’s not picking up any metadata (tableMetadata) on the @DynamoDbBean:

this.customerTable = {DefaultDynamoDbTable@4371} 
 dynamoDbClient = {DefaultDynamoDbClient@2584} 
 extension = {VersionedRecordExtension@4390} 
 tableSchema = {BeanTableSchema@4391} 
  delegateTableSchema = {StaticTableSchema@4393} 
   delegateTableSchema = {StaticImmutableTableSchema@4394} 
    attributeMappers = {Collections$UnmodifiableRandomAccessList@4395}  size = 1
    newBuilderSupplier = {LambdaToMethodBridgeBuilder$lambda@4396} 
    buildItemFunction = {Function$lambda@4397} 
    indexedMappers = {Collections$UnmodifiableMap@4398}  size = 1
    tableMetadata = {StaticTableMetadata@4399} 
     customMetadata = {Collections$UnmodifiableMap@4410}  size = 0
     indexByNameMap = {Collections$UnmodifiableMap@4411}  size = 0
     keyAttributes = {Collections$UnmodifiableMap@4412}  size = 0

To fix this, we must add setters for the partition key and sort key as follows:

import lombok.Data;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey;

@DynamoDbBean
@Data
public class Customer {

    private String id;

    @DynamoDbPartitionKey
    @DynamoDbAttribute("PK")
    public String getPartitionKey() {
        return "CUSTOMER#" + this.id;
    }

    public void setPartitionKey(String partitionKey) {
        // Do nothing, this is a derived attribute
    }

    @DynamoDbSortKey
    @DynamoDbAttribute("SK")
    public String getSortKey() {
        return "A";
    }

    public void setSortKey(String sortKey) {
        // Do nothing, this is a derived attribute
    }

}

Now if we run the application it will work as expected, and if we inspect the TableSchema, we see the expected tableMetadata:

this.customerTable = {DefaultDynamoDbTable@4381} 
 dynamoDbClient = {DefaultDynamoDbClient@2614} 
 extension = {VersionedRecordExtension@4400} 
 tableSchema = {BeanTableSchema@4401} 
  delegateTableSchema = {StaticTableSchema@4403} 
   delegateTableSchema = {StaticImmutableTableSchema@4404} 
    attributeMappers = {Collections$UnmodifiableRandomAccessList@4405}  size = 3
    newBuilderSupplier = {LambdaToMethodBridgeBuilder$lambda@4406} 
    buildItemFunction = {Function$lambda@4407} 
    indexedMappers = {Collections$UnmodifiableMap@4408}  size = 3
    tableMetadata = {StaticTableMetadata@4409} 
     customMetadata = {Collections$UnmodifiableMap@4415}  size = 0
     indexByNameMap = {Collections$UnmodifiableMap@4416}  size = 1
      "$PRIMARY_INDEX" -> {StaticIndexMetadata@4430} 
     keyAttributes = {Collections$UnmodifiableMap@4417}  size = 2
      "PK" -> {StaticKeyAttributeMetadata@4423} 
      "SK" -> {StaticKeyAttributeMetadata@4425} 

But that’s not all. Suppose you don’t add a setter for a non-primary key attribute, say, a derived attribute like Type:

import lombok.Data;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;

@DynamoDbBean
@Data
public class Customer {

    // Primary key attributes omitted
  
    @DynamoDbAttribute("Type")
    public String getType() {
        return "Customer";
    }

}

In this case, the SDK will silently ignore the Type attribute, and it will not be persisted at all. Just like we saw above, to fix this, we must add a complimentary setter for every attribute, even for derived attributes where the setter does nothing:

import lombok.Data;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;

@DynamoDbBean
@Data
public class Customer {

    // Primary key attributes omitted
  
    @DynamoDbAttribute("Type")
    public String getType() {
        return "Customer";
    }

    public void setType(String type) {
      // Do nothing, this is a derived attribute
    }
}

Detailed Exception Messages and Stack Traces

I noticed the slightly different exception messages depending on which attribute was missing the public-accessible setter, so I’m detailing them all here.

If there is no publicly-accessible setter for the partition key:

java.lang.IllegalArgumentException: Attempt to execute an operation against an index that requires a partition key without assigning a partition key to that index. Index name: $PRIMARY_INDEX
  at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata.indexPartitionKey(StaticTableMetadata.java:86)
  at software.amazon.awssdk.enhanced.dynamodb.TableMetadata.primaryPartitionKey(TableMetadata.java:121)
  at software.amazon.awssdk.enhanced.dynamodb.internal.operations.CreateTableOperation.generateRequest(CreateTableOperation.java:64)
  at software.amazon.awssdk.enhanced.dynamodb.internal.operations.CreateTableOperation.generateRequest(CreateTableOperation.java:43)
  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.createTable(DefaultDynamoDbTable.java:97)
  at software.amazon.awssdk.enhanced.dynamodb.internal.client.DefaultDynamoDbTable.createTable(DefaultDynamoDbTable.java:104)
  at com.davidagood.awssdkv2.dynamodb.repository.DynamoDbIntegrationTest.createTable(DynamoDbIntegrationTest.java:21)

If there is no publicly-accessible setter for the sort key:

java.lang.IllegalArgumentException: A sort key value was supplied for an index that does not support one. Index: $PRIMARY_INDEX
  at software.amazon.awssdk.enhanced.dynamodb.Key.lambda$keyMap$0(Key.java:70)
  at java.base/java.util.Optional.orElseThrow(Optional.java:408)
  at software.amazon.awssdk.enhanced.dynamodb.Key.keyMap(Key.java:69)
  at software.amazon.awssdk.enhanced.dynamodb.internal.operations.GetItemOperation.generateRequest(GetItemOperation.java:70)
  at software.amazon.awssdk.enhanced.dynamodb.internal.operations.GetItemOperation.generateRequest(GetItemOperation.java:35)
  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.getItem(DefaultDynamoDbTable.java:138)
  at software.amazon.awssdk.enhanced.dynamodb.internal.client.DefaultDynamoDbTable.getItem(DefaultDynamoDbTable.java:145)
  at software.amazon.awssdk.enhanced.dynamodb.internal.client.DefaultDynamoDbTable.getItem(DefaultDynamoDbTable.java:150)
  at com.davidagood.awssdkv2.dynamodb.repository.DynamoDbRepository.getCustomerById(DynamoDbRepository.java:161)
  at com.davidagood.awssdkv2.dynamodb.repository.DynamoDbIntegrationTest.insertAndRetrieveCustomer(DynamoDbIntegrationTest.java:36)

If the partition key and sort key setters are both missing or both not publicly accessible:

java.lang.IllegalArgumentException: Attempt to execute an operation that requires a primary index without defining any primary key attributes in the table metadata.
  at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata.getIndex(StaticTableMetadata.java:141)
  at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata.indexPartitionKey(StaticTableMetadata.java:78)
  at software.amazon.awssdk.enhanced.dynamodb.TableMetadata.primaryPartitionKey(TableMetadata.java:121)
  at software.amazon.awssdk.enhanced.dynamodb.internal.operations.CreateTableOperation.generateRequest(CreateTableOperation.java:64)
  at software.amazon.awssdk.enhanced.dynamodb.internal.operations.CreateTableOperation.generateRequest(CreateTableOperation.java:43)
  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.createTable(DefaultDynamoDbTable.java:97)
  at software.amazon.awssdk.enhanced.dynamodb.internal.client.DefaultDynamoDbTable.createTable(DefaultDynamoDbTable.java:104)
  at com.davidagood.awssdkv2.dynamodb.repository.DynamoDbIntegrationTest.createTable(DynamoDbIntegrationTest.java:21)

Software engineer crafting full-stack, cloud-native solutions for enterprise. GitHub | LinkedIn | Twitter