-1

I have the following classes:

public abstract class Event {
    private final String eventId;

    public Event(String eventId) {
        this.eventId = eventId;
    }
}

public class EventA extends Event {
    private final String type = "eventA";

    @JsonCreator
    public EventA(@JsonProperty("eventId") String eventId) {
        super(eventId);
    }

    public String getType() {
        return type;
    }
}

public class EventB extends Event {
    private final String type = "eventB";

    @JsonCreator
    public EventB(@JsonProperty("eventId") String eventId) {
        super(eventId);
    }

    public String getType() {
        return type;
    }
}

Problem is now I can deserialize to both classes and have an EventB with type=="eventA":

EventA eventA = new ObjectMapper().readValue(
    "{\"gameProposalId\":\"foo\", \"type\": \"eventA\"}",
    EventA.class);

// This will result in EventB with type=="eventA"
EventB eventB = new ObjectMapper().readValue(
    "{\"gameProposalId\":\"foo\", \"type\": \"eventA\"}",
    EventB.class);

Is there a way to instead compare the value of the type field in the json to the default value of type in the class you are trying to deserialize to, and throw an exception if they don't match?

diridev
  • 75
  • 7
  • So, if type is "eventA" it can't be mapped to an EventB object, right? – Diego Borba Aug 14 '23 at 18:15
  • Does this answer your question? [Jackson deserialization of type with different objects](https://stackoverflow.com/questions/20143114/jackson-deserialization-of-type-with-different-objects) – Ale Zalazar Aug 14 '23 at 18:58
  • @DiegoBorba right now it can, instead I'd like the second call to `readValue` in the code above to throw an exception. – diridev Aug 14 '23 at 19:02
  • @AleZalazar I think in that question the OP wants to deserialize 2 different json to the same class. – diridev Aug 14 '23 at 19:11
  • The `type` is used for something or just to check the type that you want to `readValue`? – Diego Borba Aug 14 '23 at 19:15
  • @DiegoBorba yes I call `getType()` somewhere else in the program. Using `type` to also solve this particular problem is just something I thought could maybe be possible, but any other solution would of course be ok as well. – diridev Aug 14 '23 at 19:21
  • @diridev The referred question might be different but its proposed solutions can solve this question, or in general provides means to customize deserialization behavior as desired, which is the actual meaning of the question. – Ale Zalazar Aug 14 '23 at 19:27
  • @AleZalazar okay so maybe it could work writing a deserializer for the `Event` class that instantiates the correct subclass based on `type` in the json. Then you could downcast it to the actual subclass like `EventA eventA = (EventA) mapper.readValue(jsonString, Event.class)`. – diridev Aug 14 '23 at 20:16

1 Answers1

1

In my opinion, the best solution is customize deserialization, this way:

EventDeserializer
public class EventDeserializer<T extends Event> extends StdDeserializer<T> {

    public EventDeserializer() {
        this(null);
    }

    public EventDeserializer(Class<?> vc) {
        super(vc);
    }

    @Override
    public T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        // Node
        JsonNode node = jp.getCodec().readTree(jp);

        // Fields
        String eventId = node.get("gameProposalId").asText();
        String type = node.get("type").asText();

        // Automatically checks the type
        if (type.equals("eventA")) {
            return (T) new EventA(eventId);

        } else if (type.equals("eventB")) {
            return (T) new EventB(eventId);

        } else {
            return null;
        }
    }
}

The EventDeserializer automatically checks the type based on the type field.

For it to work, you need to change the annotations in your classes. This way:

Event
@JsonDeserialize(using = EventDeserializer.class)
public abstract class Event {
    private final String eventId;

    public Event(String eventId) {
        this.eventId = eventId;
    }
}

The @JsonDeserialize annotation specify the custom deserializer class.

So now you dont need the @JsonCreator and @JsonProperty annotations:

EventA
public class EventA extends Event {
    private final String type = "eventA";

    public EventA(String eventId) {
        super(eventId);
    }

    public String getType() {
        return type;
    }
}
EventB
public class EventB extends Event {
    private final String type = "eventB";

    public EventB(String eventId) {
        super(eventId);
    }

    public String getType() {
        return type;
    }
}

Now, if you try to instance a EventB with one JSON with type == "eventA", it will throw a java.lang.ClassCastException:

final String json = "{\"gameProposalId\":\"foo\", \"type\": \"eventA\"}";

// Instance the object normaly
EventA eventA = new ObjectMapper().readValue(json, EventA.class);

// Throws: java.lang.ClassCastException: EventA cannot be cast to EventB
EventB eventB = new ObjectMapper().readValue(json, EventB.class);

I hope it helps :)

Diego Borba
  • 1,282
  • 8
  • 22