lschuetze lschuetze - 3 months ago 19
JSON Question

Gson deserialize polymorphic member List variable is null

Following is the class diagram of the problem domain. We have JSON decoded messages with different semantics which trigger different methods in the code (intialize, update) for different views.

The

Message
is deserialized fine into either
InitMessage
or
DataMessage
using the solution proposed here using a
RuntimeTypeAdapterFactory
and registering all possible subtypes. However, the
DataMessage.value
list is empty (not deserialized). The problem is the nested polymorphic member in
DataMessage
.

Class diagram of the problem domain

The adapter factories:

RuntimeTypeAdapterFactory<Message> messageAdapterFactory = RuntimeTypeAdapterFactory
.of(Message.class, "MESSAGE_TYPE")
.registerSubtype(InitializationMessage.class, "INIT")
.registerSubtype(DataMessage.class, "DATA");

RuntimeTypeAdapterFactory<DataValue> dataAdapterFactory = RuntimeTypeAdapterFactory
.of(DataValue.class, "NAME")
.registerSubtype(DataValueA.class, "A")
.registerSubtype(DataValueB.class, "B")
.registerSubtype(DataValueC.class, "C");


The creation of the message:

TypeToken<Message> typeToken = new TypeToken<Message>() {};
Message msg = gson.fromJson(json, typeToken.getType());


DataMessage class:

public class DataMessage extends Message {

private List<DataValue> value;

public List<DataValue> getValue() {
return value;
}

public void setValue(List<DataValue> value) {
this.value= value;
}
}


DataValueA class:

public class DataValueA extends DataValue {

private Map<String, Float> value;

public float getValue(String location) {
return value.get(location);
}
}


The corresponding JSON:

{
"MESSAGE_TYPE" : "DATA",
"VALUE" : [
{
"NAME" : "C",
"VALUE" : 1.3
},
{
"NAME" : "A",
"VALUE" : {
"FL" : 18.4,
"FR" : 18.4,
"RL" : 18.4,
"RR" : 18.4
}
}]
}


I want the
DataValue
be deserialized into their respective subclass (
DataValueA
...).

Answer

A solution is to use the GsonBuilder.registerTypeAdapter method to register custom JsonDeserializer. The way is to use a field in the message to define which subclass will be created (just like with RuntimeTypeAdapterFactory which is not shipped by default and lives in gson-extra).

The deserializer will be registered for each abstract superclass.

gson = new GsonBuilder()
  .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
  .registerTypeAdapter(Message.class, new MessageAdapter())
  .registerTypeAdapter(DataValue.class, new DataValueAdapter())
  .create();

Given the field to distinguish subtypes is named NAME you can define the deserialize function as following. There is a mapping from the content of the field to the respective subclass.

public class DataValueAdapter implements JsonDeserializer<DataValue> {
  private final static Map<String, Class<?>> FieldToClass;

  static {
    FieldToClass = new HashMap<>();
    FieldToClass.put("PERFORMANCE", PerformanceDataValue.class);
    FieldToClass.put("TIRE_SLIP", TireSlipDataValue.class);
  }

  @Override
  public DataValue deserialize(JsonElement json, Type typeOfT,
                               JsonDeserializationContext context) throws JsonParseException {
    JsonObject jsonObject = json.getAsJsonObject();
    String dataType = jsonObject.get("NAME").getAsString();
    return context.deserialize(json, FieldToClass.get(dataType));
  }
}

In order to make the reflective deserializer (which will be used for the subclasses as long as you are okay with the standard deserializer) work the subclasses need to state @SerializedName at the properties. Without it did not work for me.

Comments