Introducing the Enhanced Document API for DynamoDB in the AWS SDK for Java 2.x

May 27, 2023 By Mark Otto Off

We are excited to announce that the AWS SDK for Java 2.x now offers the Enhanced Document API for DynamoDB, providing an enhanced way of working with Amazon DynamoDb items.
This post covers using the Enhanced Document API for DynamoDB with the DynamoDB Enhanced Client. By using the Enhanced Document API, you can create an EnhancedDocument instance to represent an item with no fixed schema, and then use the DynamoDB Enhanced Client to read and write to DynamoDB.
Furthermore, unlike the Document APIs of aws-sdk-java 1.x, which provided arguments and return types that were not type-safe, the EnhancedDocument provides strongly-typed APIs for working with documents. This interface simplifies the development process and ensures that the data is correctly typed.

Prerequisites:

Before getting started, ensure you are using an up-to-date version of the AWS Java SDK dependency with all the latest released bug-fixes and features. For Enhanced Document API support, you must use version 2.20.33 or later. See our “Set up an Apache Maven project” guide for details on how to manage the AWS Java SDK dependency in your project.

Add dependency for dynamodb-enhanced in pom.xml.

<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>dynamodb-enhanced</artifactId>
<version>2.20.33</version>
</dependency>

Quick walk-through for using Enhanced Document API to interact with DDB

Step 1 : Create a DynamoDB Enhanced Client

Create an instance of the DynamoDbEnhancedClient class, which provides a high-level interface for Amazon DynamoDB that simplifies working with DynamoDB tables.

DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() .dynamoDbClient(DynamoDbClient.create()) .build();

Step 2 : Create a DynamoDbTable resource object with Document table schema

To execute commands against a DynamoDB table using the Enhanced Document API, you must associate the table with your Document table schema to create a DynamoDbTable resource object. The Document table schema builder requires the primary index key and attribute converter providers. Use AttributeConverterProvider.defaultProvider() to convert document attributes of default types. An optional secondary index key can be added to the builder.


DynamoDbTable<EnhancedDocument> documentTable = enhancedClient.table("my_table", TableSchema.documentSchemaBuilder() .addIndexPartitionKey(TableMetadata.primaryIndexName(),"hashKey", AttributeValueType.S) .addIndexSortKey(TableMetadata.primaryIndexName(), "sortKey", AttributeValueType.N) .attributeConverterProviders(AttributeConverterProvider.defaultProvider()) .build()); // call documentTable.createTable() if "my_table" does not exist in DynamoDB

Step 3 : Write a DynamoDB item using an EnhancedDocument

The EnhancedDocument class has static factory methods along with a builder method to add attributes to a document. The following snippet demonstrates the type safety provided by EnhancedDocument when you construct a document item.

EnhancedDocument simpleDoc = EnhancedDocument.builder() .attributeConverterProviders(defaultProvider()) .putString("hashKey", "sampleHash") .putNull("nullKey") .putNumber("sortKey", 1.0) .putBytes("byte", SdkBytes.fromUtf8String("a")) .putBoolean("booleanKey", true) .build(); documentTable.putItem(simpleDoc);

Step 4 : Read a Dynamo DB item as an EnhancedDocument

Attributes of the Documents retrieved from a DynamoDB table can be accessed with getter methods

EnhancedDocument docGetItem = documentTable.getItem(r -> r.key(k -> k.partitionValue("samppleHash").sortValue(1))); docGetItem.getString("hashKey");
docGetItem.isNull("nullKey")
docGetItem.getNumber("sortKey").floatValue();
docGetItem.getBytes("byte");
docGetItem.getBoolean("booleanKey"); 

AttributeConverterProviders for accessing document attributes as custom objects

You can provide a custom AttributeConverterProvider instance to an EnhancedDocument to convert document attributes to a specific object type.
These providers can be set on either DocumentTableSchema or EnhancedDocument to read or write attributes as custom objects.

TableSchema.documentSchemaBuilder() .attributeConverterProviders(CustomClassConverterProvider.create(), defaultProvider()) .build(); // Insert a custom class instance into an EnhancedDocument as attribute 'customMapOfAttribute'.
EnhancedDocument customAttributeDocument =
EnhancedDocument.builder().put("customMapOfAttribute", customClassInstance, CustomClass.class).build(); // Retrieve attribute 'customMapOfAttribute' as CustomClass object.
CustomClass customClassObject = customAttributeDocument.get("customMapOfAttribute", CustomClass.class);

Convert Documents to JSON and vice-versa

The Enhanced Document API allows you to convert a JSON string to an EnhancedDocument and vice-versa.

// Enhanced document created from JSON string using defaultConverterProviders.
EnhancedDocument documentFromJson = EnhancedDocument.fromJson("{\"key\": \"Value\"}") // Converting an EnhancedDocument to JSON string "{\"key\": \"Value\"}" String jsonFromDocument = documentFromJson.toJson();

Define a Custom Attribute Converter Provider

Custom attribute converter providers are implementations of AttributeConverterProvider that provide converters for custom classes.
Below is an example for a CustomClassForDocumentAPI which has as a single field stringAttribute of type String and its corresponding AttributeConverterProvider implementation.

public class CustomClassForDocumentAPI { private final String stringAttribute; public CustomClassForDocumentAPI(Builder builder) { this.stringAttribute = builder.stringAttribute; } public static Builder builder() { return new Builder(); } public String stringAttribute() { return stringAttribute; } public static final class Builder { private String stringAttribute; private Builder() { } public Builder stringAttribute(String stringAttribute) { this.stringAttribute = string; return this; } public CustomClassForDocumentAPI build() { return new CustomClassForDocumentAPI(this); } }
}
import java.util.Map;
import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter;
import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider;
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
import software.amazon.awssdk.utils.ImmutableMap; public class CustomAttributeForDocumentConverterProvider implements AttributeConverterProvider { private final Map<EnhancedType<?>, AttributeConverter<?>> converterCache = ImmutableMap.of( EnhancedType.of(CustomClassForDocumentAPI.class), new CustomClassForDocumentAttributeConverter()); // Different types of converters can be added to this map. public static CustomAttributeForDocumentConverterProvider create() { return new CustomAttributeForDocumentConverterProvider(); } @Override public <T> AttributeConverter<T> converterFor(EnhancedType<T> enhancedType) { return (AttributeConverter<T>) converterCache.get(enhancedType); }
}

A custom attribute converter is an implementation of AttributeConverter that converts a custom classes to and from a map of attribute values, as shown below.

import java.util.LinkedHashMap;
import java.util.Map;
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.converter.attribute.EnhancedAttributeValue;
import software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.StringAttributeConverter;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue; public class CustomClassForDocumentAttributeConverter implements AttributeConverter<CustomClassForDocumentAPI> { public static CustomClassForDocumentAttributeConverter create() { return new CustomClassForDocumentAttributeConverter(); } @Override public AttributeValue transformFrom(CustomClassForDocumentAPI input) { Map<String, AttributeValue> attributeValueMap = new LinkedHashMap<>(); if(input.string() != null){ attributeValueMap.put("stringAttribute", AttributeValue.fromS(input.string())); } return EnhancedAttributeValue.fromMap(attributeValueMap).toAttributeValue(); } @Override public CustomClassForDocumentAPI transformTo(AttributeValue input) { Map<String, AttributeValue> customAttr = input.m(); CustomClassForDocumentAPI.Builder builder = CustomClassForDocumentAPI.builder(); if (customAttr.get("stringAttribute") != null) { builder.stringAttribute(StringAttributeConverter.create().transformTo(customAttr.get("stringAttribute"))); } return builder.build(); } @Override public EnhancedType<CustomClassForDocumentAPI> type() { return EnhancedType.of(CustomClassForDocumentAPI.class); } @Override public AttributeValueType attributeValueType() { return AttributeValueType.M; }
}

Attribute Converter Provider for EnhancedDocument Builder

When working outside of a DynamoDB table context, make sure to set the attribute converter providers explicitly on the EnhancedDocument builder. When used within a DynamoDB table context, the table schema’s converter provider will be used automatically for the EnhancedDocument.
The code snippet below shows how to set an AttributeConverterProvider using the EnhancedDocument builder method.

// Enhanced document created from JSON string using custom AttributeConverterProvider.
EnhancedDocument documentFromJson = EnhancedDocument.builder() .attributeConverterProviders(CustomClassConverterProvider.create()) .json("{\"key\": \"Values\"}") .build(); CustomClassForDocumentAPI customClass = documentFromJson.get("key", CustomClassForDocumentAPI.class)

Conclusion

In this blog post we showed you how to set up and begin using the Enhanced Document API with the DynamoDB Enhanced Client and standalone with the EnhancedDocument class. The enhanced client is open-source and resides in the same repository as the AWS SDK for Java 2.0.
We hope you’ll find this new feature useful. You can always share your feedback on our GitHub issues page.