DynamoDB JsonMarshaller cannot Deserialize List of

2020-01-31 07:23发布

问题:

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

The list can be successfully serialized by the JsonMarshaller. However, when I try to retrieve the entry back and read the list, it throws an exception: java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to MyObject. It looks like that the JsonMarshaller de-serialize the data into the LinkedHashMap instead of MyObject. How can I get rid of this problem?

The 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
    }
  }
}

回答1:

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

You need to provide a better implementation for your deserialization. One way you could do this would be to implement the DynamoDBMarshaller interface rather than extending the other one (my opinion) so you have better control over how the type is serialized.

Here is an example that is essentially copy/paste of the JsonMarshaller, with minor tweaks in 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);
        }
    }
}


回答2:

In newer versions simply works with:

@DynamoDBAttribute(attributeName = "things")
public List<Thing> getThings() {
    return things;
}

public void setThings(final List<Thing> things) {
    this.things = things;
}

given that the Thing is adnotated with:

@DynamoDBDocument
public class Thing {
}


回答3:

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

Here is the 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;
    }
}


回答4:

Interface DynamoDBMarshaller<T extends Object> is deprecated already, the replacement is Interface DynamoDBTypeConverter<S,T>.

Inside your model class, add the annotation to your 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;
        }
    }  
}


回答5:

I've found that the response by Aleris works fine. In my example, I have a dynamo db table that contains two collections, both of non-primitive classes.

After trying various flavours of DBTypeConverters (taking {String, MyObject}, {Collection, Collection}, {String, Collection}) and also trying Set rather than Collection, by merely annotating the referred to class as a DynamoDBDocument, that I could pass a json array of data for those child classes and the data was persisted correctly.

my "parent class" looks like this (names altered 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. ImageRef, Presentation and Dimensions classes are all tagged with the DynamoDBDocument annotation.