25

I have an abstract class called Instance and then two implementations of that, UserInstance and HardwareInstance. The issue I am having is that when I call the rest endpoint for a @POST into the database, I ideally wanted it to be like .../rest/soexample/instance/create where the instance is passed to the REST endpoint. If Instance wasn't abstract with more than one implementation it would be fine, but since I have 2 I am getting a Jackson.databind error.

" problem: abstract types either need to be mapped to concrete types, have custom deserializer, or be instantiated with additional type information"

After looking up a solution to this I found a SO answer that said I could use something like:

@JsonDeserialize(as=UserInstance.class)

But it seem's like that isonly useful if there is one implementation of the abstract class. Assuming I can't call it twice since there would be no way for it to decide which type of instance it would be.

So I am wondering what is the best way to handle this situation? Should I create different endpoints? Like:

.../rest/soexample/userinstance/create & .../rest/soexample/hardwareinstance/create

I am not too sure as I am a noobie @ REST related things, though actively trying to learn. Thanks!

erp
  • 2,950
  • 9
  • 45
  • 90
  • You could write a custom deserializer for that abstract class, which would have to programmatically parse the JSON depending on the fields and parse the body as whatever concrete class you had in mind. – IanGabes Sep 24 '15 at 17:02
  • Thanks for the suggestion. I have never done that before. Where would I place the custom deserializer? Like would I call it from inside the endpoint for `.../rest/soexample/instance/create`? – erp Sep 24 '15 at 17:05
  • REST is a non-specific type of application, it doesn't tell me much about what server/client technology you are using. I found a SO question similiar to yours with the solution that i had in mind: http://stackoverflow.com/questions/8210538/dynamic-polymorphic-type-handling-with-jackson I would generally put the deserializer as a public static inner class of the object you are trying to deserialize. – IanGabes Sep 24 '15 at 18:03

1 Answers1

33

Here is what I did in your same case:

@JsonDeserialize(using = InstanceDeserializer.class)
public abstract class Instance {
    //.. methods
}

@JsonDeserialize(as = UserInstance.class)
public class UserInstance extends Instance {
    //.. methods
}

@JsonDeserialize(as = HardwareInstance.class)
public class HardwareInstance extends Instance {
    //.. methods
}

public class InstanceDeserializer extends JsonDeserializer<Instance> {
    @Override
    public Instance deserialize(JsonParser jp,  DeserializationContext ctxt) throws IOException, JsonProcessingException {
        ObjectMapper mapper = (ObjectMapper) jp.getCodec();
        ObjectNode root = (ObjectNode) mapper.readTree(jp);
        Class<? extends Instance> instanceClass = null;
        if(checkConditionsForUserInstance()) {
            instanceClass = UserInstance.class;
        } else { 
            instanceClass = HardwareInstance.class;
        }   
        if (instanceClass == null){
            return null;
        }
        return mapper.readValue(root, instanceClass );
    }
}

You annotate Instance with @JsonDeserialize(using = InstanceDeserializer.class) to indicate the class to be used to deserialize the abstract class. You need then to indicate that each child class will be deserialized as themselves, otherwise they will use the parent class deserializer and you will get a StackOverflowError.

Finally, inside the InstanceDeserializer you put the logic to deserialize into one or another child class (checkConditionsForUserInstance() for example).

carcaret
  • 3,238
  • 2
  • 19
  • 37
  • 1
    What should be the logic in checkConditionsForUserInstance() method? Do we get this instance information in ctxt? – Pankaj Dwivedi Sep 21 '16 at 08:24
  • 1
    No, it's just a method that you do yourself to differentiate into which class you want to deserialize de Json. – carcaret Sep 21 '16 at 08:49
  • 1
    Ok. my situation is that I am not sure about the instance classes as I am writing an SDK and classes will be implemented by developers for the interface present in SDK. Any idea how to deal with it in this scenario, where i am not aware of the class but i have to convert the Json into the interface type? – Pankaj Dwivedi Sep 21 '16 at 14:29
  • 3
    At this point, you must have some mechanism to differentiate the concrete class associated with the Json. If child classes are out of your control like you say, maybe you should add an extra mandatory parameter to the Json that tells you the concrete class you need. You can also check the `@JsonTypeInfo` annotation as well, but I think it won't fit in your scenario. Ultimately, although quite complex, you can scan all implementing classes of your interface using reflection, and match which one has the same fields as the Json, and then deserialize into that one. I can't think of anything else. – carcaret Sep 22 '16 at 08:39
  • 4
    Thanks a lot!! I did resolve this issue by using @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "type") on top of my interface. It adds a type field in the json its saving and while deserializing it makes use of that value. So it is doing exactly what you have suggested. – Pankaj Dwivedi Sep 22 '16 at 08:50
  • Hi @carcaret, your answer looks interesting. However, even if we have a `Instance.getTheType()` method, how do we call it as we don't have an object of `Instance` class in the above example? – ericn Oct 16 '16 at 11:19
  • Hi @eric. The purpose of the deserializer is to tell Jackson the concrete class it will have to deserialize the received JSON into. That's why you don't have an `Instance` instance to get the type, because you don't know it yet. The idea is to read the JSON you've received inside the `checkConditionsForUserInstance()` method (through the methods in `ObjectNode` class), and based on your own conditions (presence or absence of some field, type of a specific field...), select the concrete class it'll be deserialized into and asign it to the `instanceClass` variable. – carcaret Oct 17 '16 at 10:34
  • 5
    Shouldn't the last line be `return mapper.convertValue(root, instanceClass);` – Conor Svensson Nov 03 '16 at 04:00
  • The last line works for me as `return mapper.readValue(jp, instanceClass );` – Troy Daniels Sep 14 '17 at 23:16
  • OK. That works only if you know all extended classes, but what happens in case you did not know them? I mean, i.e.m you have INTERFACE in one COMMON package, and many implementations in other packages, and you do not know how their names (in the interface package) what to do in this case ? – Andrew Niken Jul 19 '18 at 12:01
  • 1
    For Jackson 2.x use `return mapper.treeToValue(root, instanceClass);` instead `return mapper.readValue(root, instanceClass );` – Rafi Mar 26 '19 at 13:08
  • @carcaret - What if the "Instance" abstract class object is present in some parent class "Parent" and to detect the concrete class of abstract type or to implement checkConditionsForUserInstance() we need some fields from parent..And VERY IMPORTANT is after following above approach the BigDecimal values are converting to Double which is causing the issue .. So how can I get the parent class fields as well as can maintain fields datatypes as is? – Ganesh Nov 11 '20 at 11:40
  • @Rafi - What if the "Instance" abstract class object is present in some parent class "Parent" and to detect the concrete class of abstract type or to implement checkConditionsForUserInstance() we need some fields from parent..And VERY IMPORTANT is after following above approach the BigDecimal values are converting to Double which is causing the issue .. So how can I get the parent class fields as well as can maintain fields datatypes as is? – Ganesh Nov 11 '20 at 11:40