DynamoDB JsonMarshaller cannot deserialize a list of objects

I have a Java class that is a table data model in DynamoDB. I want to use DynamoDBMapper to save and load elements from Dynamo. One member of the class is List<MyObject> . So I used JsonMarshaller<List<MyObject>> to serialize and de-serialize this field.

The list can be serialized successfully by JsonMarshaller . However, when I try to restore the record and read the list, it throws an exception: java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to MyObject . It seems that JsonMarshaller deserializing the data in LinkedHashMap instead of MyObject . How can I get rid of this problem?

MCVE:

 // Model.java @DynamoDBTable(tableName = "...") public class Model { private String id; private List<MyObject> objects; public Model(String id, List<MyObject> objects) { this.id = id; this.objects = objects; } @DynamoDBHashKey(attributeName = "id") public String getId() { return this.id; } public void setId(String id) { this.id = id; } @DynamoDBMarshalling(marshallerClass = ObjectListMarshaller.class) public List<MyObject> getObjects() { return this.objects; } public void setObjects(List<MyObject> objects) { this.objects = objects; } } 

 // MyObject.java public class MyObject { private String name; private String property; public MyObject() { } public MyObject(String name, String property) { this.name = name; this.property = property; } public String getName() { return this.name; } public void setName(String name) { this.name = name; } public String getProperty() { return this.property; } public void setProperty(String property) { this.property = property; } } 

 // ObjectListMarshaller.java public class ObjectListMarshaller extends JsonMarshaller<List<MyObject>> {} 

 // Test.java public class Test { private static DynamoDBMapper mapper; static { AmazonDynamoDBClient client = new AmazonDynamoDBClient(new ProfileCredentialsProvider() mapper = new DynamoDBMapper(client); } public static void main(String[] args) { MyObject obj1 = new MyObject("name1", "property1"); MyObject obj2 = new MyObject("name2", "property2"); List<MyObject> objs = Arrays.asList(obj1, obj2); Model model = new Model("id1", objs); mapper.save(model); // success Model retrieved = mapper.load(Model.class, "id1"); for (MyObject obj : retrieved.getObjects()) { // exception } } } 
+8
source share
5 answers

Part of the problem here is how the whole DynamoDB Mapper SDK deals with generics. interface DynamoDBMarshaller<T extends Object> has a method T unmarshall(Class<T> clazz, String obj) in which the class deserialize to is passed as a parameter. The problem is that there is type erasure , and the SDK does not provide the ease to handle this. In some cases, Jackson is smarter ( JsonMarshaller uses Jackson), which explains why the serialize method works correctly.

You need to provide a better implementation for deserialization. One way to do this is to implement the DynamoDBMarshaller interface, and not extend it (my opinion) so that you can better control how the type is serialized.

Here is an example, which is essentially a copy / paste of JsonMarshaller , with minor changes to the deserialization for the List to give you an idea:

 import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMarshaller; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.type.CollectionType; import java.util.List; import static com.amazonaws.util.Throwables.failure; public class MyCustomMarshaller implements DynamoDBMarshaller<List<MyObject>> { private static final ObjectMapper mapper = new ObjectMapper(); private static final ObjectWriter writer = mapper.writer(); @Override public String marshall(List<MyObject> obj) { try { return writer.writeValueAsString(obj); } catch (JsonProcessingException e) { throw failure(e, "Unable to marshall the instance of " + obj.getClass() + "into a string"); } } @Override public List<MyObject> unmarshall(Class<List<MyObject>> clazz, String json) { final CollectionType type = mapper.getTypeFactory().constructCollectionType(List.class, MyObject.class); try { return mapper.readValue(json, type); } catch (Exception e) { throw failure(e, "Unable to unmarshall the string " + json + "into " + clazz); } } } 
+7
source

In new versions it just works with:

 @DynamoDBAttribute(attributeName = "things") public List<Thing> getThings() { return things; } public void setThings(final List<Thing> things) { this.things = things; } 

given that the item is marked:

 @DynamoDBDocument public class Thing { } 
+7
source

DynamoDBMarshaller is now deprecated, but I get exactly the same problem with DynamoDBTypeConvertedJson. If you want to save the collection as JSON in the DynamoDBMapper class, use DynamoDBTypeConverted and write your own converter class (do not use DynamoDBTypeConvertedJson, which will not return your collection when unconvert).

Here is a solution using DynamoDBTypeConverted

 // Model.java @DynamoDBTable(tableName = "...") public class Model { private String id; private List<MyObject> objects; public Model(String id, List<MyObject> objects) { this.id = id; this.objects = objects; } @DynamoDBHashKey(attributeName = "id") public String getId() { return this.id; } public void setId(String id) { this.id = id; } @DynamoDBTypeConverted(converter = MyObjectConverter.class) public List<MyObject> getObjects() { return this.objects; } public void setObjects(List<MyObject> objects) { this.objects = objects; } } 

-

 public class MyObjectConverter implements DynamoDBTypeConverter<String, List<MyObject>> { @Override public String convert(List<Object> objects) { //Jackson object mapper ObjectMapper objectMapper = new ObjectMapper(); try { String objectsString = objectMapper.writeValueAsString(objects); return objectsString; } catch (JsonProcessingException e) { //do something } return null; } @Override public List<Object> unconvert(String objectssString) { ObjectMapper objectMapper = new ObjectMapper(); try { List<Object> objects = objectMapper.readValue(objectsString, new TypeReference<List<Object>>(){}); return objects; } catch (JsonParseException e) { //do something } catch (JsonMappingException e) { //do something } catch (IOException e) { //do something } return null; } } 
+1
source

Interface DynamoDBMarshaller<T extends Object> already deprecated, replacing Interface DynamoDBTypeConverter<S,T> .

Inside the model class, add the annotation to the list of objects.

 @DynamoDBTypeConverted(converter = PhoneNumberConverter.class) public List<MyObject> getObjects() { return this.objects; } 

public void setObjects (list objects) {this.objects = objects; }

And this is the implementation of DynamoDBTypeConverter .

 public class PhoneNumberConverterimplements DynamoDBTypeConverter<String, PhoneNumber> { private static final ObjectMapper mapper = new ObjectMapper(); private static final ObjectWriter writer = mapper.writerWithType(new TypeReference<List<MyObject>>(){}); @Override public String convert(List<MyObject> obj) { try { return writer.writeValueAsString(obj); } catch (JsonProcessingException e) { System.out.println( "Unable to marshall the instance of " + obj.getClass() + "into a string"); return null; } } @Override public List<MyObject> unconvert(String s) { TypeReference<List<MyObject>> type = new TypeReference<List<MyObject>>() {}; try { List<MyObject> list = mapper.readValue(s, type); return list; } catch (Exception e) { System.out.println("Unable to unmarshall the string " + s + "into " + s); return null; } } } 
0
source

I found that Aleris answer works fine. In my example, I have a dynamo-db table that contains two collections, both from non-primitive classes.

Try different DBTypeConverters variations (taking {String, MyObject}, {Collection, Collection}, {String, Collection}), and also try Set, not Collection, just annotating the mentioned class as DynamoDBDocument so that I can pass a json array of data for these child classes, and the data was saved correctly.

my “parent class” looks like this (names changed to protect the innocent);

 @DynamoDBTable(tableName = "SomeTable") public class ParentClass { @NotNull(message = "Key must be specified") @Size(min = 12, max = 20) @DynamoDBHashKey private String key; private String type; @NotNull(message = "name must be specified.") private String name; @NotNull(message = "Type code must be specified") @DynamoDBTyped(DynamoDBMapperFieldModel.DynamoDBAttributeType.S) private TypeCode typeCode; private List<ImageRef> images; /** * Optional physical dimensions */ private Dimensions productDimensions; /** * Optional presentations. */ private Set<Presentation> presentations; } 

TypeCode is an enumeration. All ImageRef, Presentation, and Dimensions classes are marked with the DynamoDBDocument annotation.

0
source

Source: https://habr.com/ru/post/988960/


All Articles