jocopa3
jocopa3

Reputation: 796

GSON Serialize Polymorphic Object with Type Stored in a Different Object

To start off, I have looked at a few other answers for similar questions, but they do not answer my particular situation.

I'm parsing JSON messages which consist of a body and a header, where the header stores what type of object the body is:

{
    "body": {
        "eventName": "someEventName"
    },
    "header": {
        "purpose": "event"
    }
}

In Java, I've modeled this structure using the following classes:

public class Message {
    public Body body;
    public Header header;
}

public class Header {
    public String purpose; // Marks what child class the body of the message uses
}

public abstract class Body {
    // Child classes store additional fields
}

// Example implementation of the body class
public class EventBody extends Body {
    public String eventName; // Name of some event
}

After doing some research, I found that RuntimeTypeAdapterFactory is normally used to parse/write polymorphic objects; however, the RutimeTypeAdapterFactory class relies on the type being stored in the base class of the polymorphic object (i.e. Body). But in this scenario, that's not the case ― the type is stored in another object, Header.

What would be the best way to go about parsing these kind of objects? I'd like to avoid having to write a custom Serializer/Deserializer for compactness, but I wouldn't mind writing them if it's necessary.

Upvotes: 0

Views: 2239

Answers (1)

jocopa3
jocopa3

Reputation: 796

I realize that asking for a solution that doesn't involve a custom Serializer/Deserializer is a bit ridiculous, as this is exactly the type of scenario they'd be used in (I was thinking I could get away with a custom TypeAdapterFactory, but using a Serializer/Deserializer is easier).

Anyway, for my scenario, a combination of a custom Serializer/Deserializer for the Message class seems to work fine. Since I already use an enum to track different message purposes and their string names, I decided to simply add an additional field to that enum to store the corresponding body class.

MessagePurpose Enum:

public enum MessagePurpose {
    EVENT("event", EventBody.class);

    public final String purposeName;
    public final Class bodyClass;

    MessagePurpose(String purposeName, Class classi) {
        this.purposeName = purposeName;
        bodyClass = classi;
    }
}

MessageSerializer:

public class MessageSerializer implements JsonSerializer<Message> {
    @Override
    public JsonElement serialize(Message message, Type type, JsonSerializationContext jsc) {
        if(message == null) {
            return null;
        }

        JsonObject messageObj = new JsonObject();

        // Get the class representing the body object from the purpose enum
        Class bodyClassType = message.getPurpose().bodyClass;
        messageObj.add("body", jsc.serialize(message.getBody(), bodyClassType));

        messageObj.add("header", jsc.serialize(message.getHeader(), Header.class));

        return messageObj;
    }
}

MessageDeserializer:

public class MessageDeserializer implements JsonDeserializer<Message> {
    @Override
    public Message deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException {
        Header header = jdc.deserialize(je.getAsJsonObject().get("header"), Header.class);

        // Get the class representing the body object from the purpose enum
        Class bodyClassType = header.getPurpose().bodyClass;
        Body body = jdc.deserialize(je.getAsJsonObject().get("body"), bodyClassType);

        return new Message(body, header);
    }
}

Main function to test with:

public static void main(String[] args) {
    GsonBuilder gb = new GsonBuilder();

    // Register the Message class since I need to access info in the header
    gb.registerTypeAdapter(Message.class, new MessageDeserializer());
    gb.registerTypeAdapter(Message.class, new MessageSerializer());

    Gson gson = gb.setPrettyPrinting().create();

    EventBody event = new EventBody(EventType.SOME_EVENT_NAME);

    String eventJson = gson.toJson(event.getAsMessage());
    System.out.println(eventJson);

    Message newEvent = gson.fromJson(eventJson);
    System.out.println("\nEvent type: " + ((EventBody) newEvent.getBody()).getEventName());
}

The above test class prints:

{
  "body": {
    "eventType": "someEventName"
  },
  "header": {
    "purpose": "event"
  }
}

Event Type: someEventName

This output matches the JSON of the Messages I'm parsing, and it seems to deserialize different types of messages just fine.

Upvotes: 2

Related Questions