'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


Solution 1:[1]

Solution based on watkinsmatthewp Answer:

public class TaskStatusConverter implements AttributeConverter<TaskStatus> {
    @Delegate
    private final EnumAttributeConverter<TaskStatus> converter;

    public TaskStatusConverter() {
        converter = EnumAttributeConverter.create(TaskStatus.class);
    }
}

Task status attribute looks like this:

@Getter(onMethod_ = {@DynamoDbConvertedBy(TaskStatusConverter.class)})
TaskStatus status;

Solution 2:[2]

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.

Solution 3:[3]

I am thinking that your annotations might actually be the problem here. I would remove all annotations that mention a constructor, and instead, write out your own constructor(s). For both Task and TaskStatus.

Solution 4:[4]

For anyone else coming here, it looks do me like just dropping the annotation from the enum altogether works just fine, i.e. the SDK applies the provided attribute converters implicitly. This is also mentioned in this Github issue. My own class looks like this (Brand is an enum here), and the enum is converted without any issues when fetching items.

@Value
@Builder(toBuilder = true)
@DynamoDbImmutable(builder = User.UserBuilder.class)
public class User {

    @Getter(onMethod = @__({@DynamoDbPartitionKey}))
    String id;

    Brand brand;
    ...
}

Solution 5:[5]

The dynamodb-enhanced SDK does this out of the box.

When you declare a @DynamoDbBean the DefaultAttributeConverterProvider provides a long list of possible ways to convert attributes between java types, including an EnumAttributeConverter which is used if type.rawClass().isEnum() is true. So you don't need to worry about it.

If you ever wanted to extend the number of converters, you would need to add the converterProviders annotation parameter, and declare the default one (or omit it), as well as any other providers you want.

Example: @DynamoDbBean(converterProviders = { DefaultAttributeConverterProvider.class, MyCustomAttributeConverterProvider.class });

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 3Fish
Solution 2 watkinsmatthewp
Solution 3 davidalayachew
Solution 4 Daniel
Solution 5 ghostgatts