How can I use Java Enums with Amazon DynamoDB and AWS SDK v2?

I am trying to implement a simple java event-handler lambda for AWS. It receives sqs events and should make appropriate updates to the dynamoDB table.

One of the attributes in this table is a status field that has 4 defined states; therefore I wanted to use an enum class in java and map it to this attribute.

Under AWS SDK v1 I could use the @DynamoDBTypeConvertedEnum annotation. But it does not exist anymore in v2. Instead, there is the @DynamoDbConvertedBy() which receives a converter class reference. There is also an EnumAttributeConverter class which should work nicely with it.

But for some reason, it does not work. The following is a snip from my current code:

@Data
@DynamoDbBean
@NoArgsConstructor
public class Task{

@Getter(onMethod_ = {@DynamoDbPartitionKey})  
    String id; 

...

@Getter(onMethod_ = {@DynamoDbConvertedBy(EnumAttributeConverter.class)})
    ExportTaskStatus status;
}

The enum looks as follows:

@RequiredArgsConstructor
public enum TaskStatus {
    @JsonProperty("running") PROCESSING(1),
    @JsonProperty("succeeded") COMPLETED(2),
    @JsonProperty("cancelled") CANCELED(3),
    @JsonProperty("failed") FAILED(4);

    private final int order;
}

With this, I get the following exception when launching the application:

Class 'class software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.EnumAttributeConverter' appears to have no default constructor thus cannot be used with the BeanTableSchema

Answer

How can I use Java Enums with Amazon DynamoDB and AWS SDK v2?

Although the documentation doesn’t state it, the DynamoDbConvertedBy annotation requires any AttriuteConverter you supply to contain a parameterles default constructor

Unfortunately for you and me, whoever wrote many of the built-in AttributeConverter classes decided to use static create() methods to instantiate them instead of a constructor (maybe they’re singletons under the covers? I don’t know). This means anyone who wants to use these helpful constructor-less classes like InstantAsStringAttributeConverter and EnumAttributeConverter needs to wrap them in custom wrapper classes that simple parrot the converters we instantiated using create. For a non-generic typed class like InstantAsStringAttributeConverter, this is easy. Just create an wrapper class that parrots the instance you new up with create() and refer to that instead:

public class InstantAsStringAttributeConverterWithConstructor implements AttributeConverter<Instant> {
    private final static InstantAsStringAttributeConverter CONVERTER = InstantAsStringAttributeConverter.create();

    @Override
    public AttributeValue transformFrom(Instant instant) {
        return CONVERTER.transformFrom(instant);
    }

    @Override
    public Instant transformTo(AttributeValue attributeValue) {
        return CONVERTER.transformTo(attributeValue);
    }

    @Override
    public EnhancedType<Instant> type() {
        return CONVERTER.type();
    }

    @Override
    public AttributeValueType attributeValueType() {
        return CONVERTER.attributeValueType();
    }
}

Then you update your annotation to point to that class intead of the actual underlying library class.

But wait, EnumAttributeConverter is a generic typed class, which means you need to go one step further. First, you need to create a version of the converter that wraps the official version but relies on a constructor taking in the type instead of static instantiation:

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.EnumAttributeConverter;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;

public class EnumAttributeConverterWithConstructor<T extends Enum<T>> implements AttributeConverter<T> {
    private final EnumAttributeConverter<T> converter;

    public CustomEnumAttributeConverter(final Class<T> enumClass) {
        this.converter = EnumAttributeConverter.create(enumClass);
    }

    @Override
    public AttributeValue transformFrom(T t) {
        return this.converter.transformFrom(t);
    }

    @Override
    public T transformTo(AttributeValue attributeValue) {
        return this.converter.transformTo(attributeValue);
    }

    @Override
    public EnhancedType<T> type() {
        return this.converter.type();
    }

    @Override
    public AttributeValueType attributeValueType() {
        return this.converter.attributeValueType();
    }
}

But that only gets us half-way there– now we need to generate a version for each enum type we want to convert that subclasses our custom class:

public class ExportTaskStatusAttributeConverter extends EnumAttributeConverterWithConstructor<ExportTaskStatus> {
    public ExportTaskStatusAttributeConverter() {
        super(ExportTaskStatus.class);
    }
}
@DynamoDbConvertedBy(ExportTaskStatusAttributeConverter.class)
public ExportTaskStatus getStatus() { return this.status; }

Or the Lombok-y way:

@Getter(onMethod_ = {@DynamoDbConvertedBy(ExportTaskStatusAttributeConverter.class)})
ExportTaskStatus status;

It’s a pain. It’s a pain that could be solved with a little bit of tweaking and a tiny bit of reflection in the AWS SDK, but it’s where we’re at right now.