aws-sdk-java-v2 icon indicating copy to clipboard operation
aws-sdk-java-v2 copied to clipboard

DefaultStringConverterProvider can't convert Map with Enum as a key

Open bbednarek opened this issue 5 years ago • 4 comments
trafficstars

Mapping of the Map<K,String> where K is enum doesn't work.

Example entity:

@DynamoDbBean
@Data
public class TestClass {

    @DynamoDbPartitionKey
    private String id;
    private Map<Status, String> map;
   
    public enum Status {
         ONE, TWO
     }
}

Example TableSchema

private TableSchema<TestClass> tableSchema = TableSchema.fromBean(TestClass.class);

Describe the bug

Mapping doesn't work.

Expected Behavior

Mapping works.

Current Behavior

It fails with the following error:

Caused by: java.lang.IllegalArgumentException: No string converter exists for class test.TestClass.Status

Full stacktrace:

Caused by: java.lang.IllegalArgumentException: No string converter exists for class test.TestClass.Status
	at software.amazon.awssdk.enhanced.dynamodb.internal.converter.string.DefaultStringConverterProvider.converterFor(DefaultStringConverterProvider.java:155)
	at software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider.createMapConverter(DefaultAttributeConverterProvider.java:182)
	at software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider.findConverter(DefaultAttributeConverterProvider.java:149)
	at software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider.converterFor(DefaultAttributeConverterProvider.java:133)
	at software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableAttribute.converterFrom(ImmutableAttribute.java:167)
	at software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableAttribute.resolve(ImmutableAttribute.java:163)
	at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema.lambda$new$0(StaticImmutableTableSchema.java:153)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
	at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1624)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
	at java.base/java.util.stream.StreamSpliterators$WrappingSpliterator.forEachRemaining(StreamSpliterators.java:312)
	at java.base/java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:734)
	at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:658)
	at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema.<init>(StaticImmutableTableSchema.java:159)
	at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema.<init>(StaticImmutableTableSchema.java:77)
	at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema$Builder.build(StaticImmutableTableSchema.java:425)
	at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema.<init>(StaticTableSchema.java:66)
	at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema.<init>(StaticTableSchema.java:64)
	at software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema$Builder.build(StaticTableSchema.java:255)
	at software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema.createStaticTableSchema(BeanTableSchema.java:200)
	at software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema.create(BeanTableSchema.java:124)
	at software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema.create(BeanTableSchema.java:116)
	at software.amazon.awssdk.enhanced.dynamodb.TableSchema.fromBean(TableSchema.java:80)
	at test.DynamoDbTestConfiguration.<init>(DynamoDbTestConfiguration.java:22)
	at test.DynamoDbTestConfiguration$$EnhancerBySpringCGLIB$$9f051cd1.<init>(<generated>)
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:500)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:481)
	at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:204)
	... 111 more

If I swap Map key and value:

@DynamoDbBean
@Data
public class TestClass {

    @DynamoDbPartitionKey
    private String id;
    private Map<String, Status> map;
   
    public enum Status {
         ONE, TWO
     }
}

it works!

Steps to Reproduce

Follow the steps above.

Possible Solution

Support Enum as a key in a Map.

Context

I am trying to save a map with enum as key.

Your Environment

  • AWS Java SDK version used: 2.15.2
  • JDK version used: 14
  • Operating System and version: MacOS 10.15.7

Related to the comment https://github.com/aws/aws-sdk-java-v2/issues/35#issuecomment-606159193

bbednarek avatar Oct 20 '20 08:10 bbednarek

@bbednarek the default MapAttributeConverter only supports StringAttributeConverter for keys: https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/enhanced/dynamodb/internal/converter/attribute/MapAttributeConverter.html

I'm marking this as a feature request. In the meantime to get unblocked you can write a custom Attribute converter, check the DynamoDB Enhanced Client Overview for examples: https://github.com/aws/aws-sdk-java-v2/tree/master/services-custom/dynamodb-enhanced#control-attribute-conversion

debora-ito avatar Oct 22 '20 02:10 debora-ito

Recently solved a similar task using the official dynamodb-enhanced client which contains DynamoDbConvertedBy annotation. There you can specify a converter class that implements the AttributeConverter interface for your data type.

Below is just my example of converter implementation for Map<EnumClass, String> type:

@DynamoDbConvertedBy(EnumMapAttributeConverter.class) for the entity field's getter.

Class itself:

public class EnumMapAttributeConverter
    implements AttributeConverter<Map<EnumClass, String>> {

  @Override
  public AttributeValue transformFrom(final Map<EnumClass, String> input) {
    Map<String, AttributeValue> attributeValueMap =
        input.entrySet().stream()
            .collect(
                Collectors.toMap(
                    k -> k.getKey().getValue(),
                    v -> AttributeValue.builder().s(v.getValue()).build()));
    return AttributeValue.builder().m(attributeValueMap).build();
  }

  @Override
  public Map<EnumClass, String> transformTo(final AttributeValue input) {
    return input.m().entrySet().stream()
        .collect(
            Collectors.toMap(
                k -> getEnumClassKeyByString(k.getKey()), v -> v.getValue().s()));
  }

  private EnumClass getEnumClassKeyByString(final String key) {
    EnumClass enumClass = EnumClass.getByValue(key);
    return enumClass != null ? enumClass : EnumClass.NOT_FOUND;
  }

  @Override
  public EnhancedType<Map<EnumClass, String>> type() {
    return EnhancedType.mapOf(EnumClass.class, String.class);
  }

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

antukhov avatar Jan 06 '22 03:01 antukhov

@antukhov Can you provide a working example pls? I added an AttributeConverterClass, Getter-Method and the Annotation - still the same IllegalArgument No string converter exists ... exception. Thanks!

hamburml avatar Nov 03 '22 19:11 hamburml

@antukhov I did something similar just for complicated map value which is also annotated with DynamoDbBean:

public class EnumMapAttributeConverter implements AttributeConverter<Map<MVDayOfWeek, SeoLineFrequency>> {
    private static final BeanTableSchema<SeoLineFrequency> seoLineFrequencyBeanTableSchema = TableSchema.fromBean(SeoLineFrequency.class);

    @Override
    public AttributeValue transformFrom(Map<MVDayOfWeek, SeoLineFrequency> input) {
        return AttributeValue.fromM(input.entrySet().stream()
                .collect(Collectors.toMap(
                       entry -> entry.getKey().name(),
                       entry -> AttributeValue.fromM(seoLineFrequencyBeanTableSchema.itemToMap(entry.getValue(), true))
                )));
    }

    @Override
    public Map<MVDayOfWeek, SeoLineFrequency> transformTo(AttributeValue input) {
        return input.m().entrySet().stream()
                .collect(Collectors.toMap(
                        entry -> MVDayOfWeek.valueOf(entry.getKey()),
                        entry -> seoLineFrequencyBeanTableSchema.mapToItem(entry.getValue().m())
                ));
    }

    @Override
    public EnhancedType<Map<MVDayOfWeek, SeoLineFrequency>> type() {
        return EnhancedType.mapOf(MVDayOfWeek.class, SeoLineFrequency.class);
    }

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

ashr123 avatar Apr 01 '24 16:04 ashr123