I have an interesting problem that I’m having trouble coming up with a clean solution for. My application reads collections of json objects that it needs to deserialize to this or that class type based on a field in the json itself. I have no control over the json structure or how it gets to my application.
I’ve created models for each type of object that could be coming to the application and I’ve reached a point where I’m trying to build a service that pulls out the ‘type’ field and then uses ObjectMapper to deserialize the json to the appropriate Model.
Json example:
{ "message_type" : "model1" "other data" : "other value" ... }
Models:
public class Model1 { ... } public class Model2 { ... }
Service?:
public class DynamicMappingService { public ???? mapJsonToObject(String json) { String type = pullTypeFromJson(); ??? } private String pullTypeFromJson() {...} }
I don’t want a massive switch statement that says “If type value is this then deserialize to that” but I’m struggling to come up with something clean that does that. I thought maybe a generic model class where the generic parameter is the Model Type and the only field is the instance of that model type but that doesn’t seem right either and I’m not sure what that buys me. I could also have some sort of empty abstract class that all of the models extend but that seems horrible too. How do I deal with this? Extra points for an example.
Advertisement
Answer
I use the concept of a parent interface Vehicle with 2 classes Car and Truck. In your case this means Model1 and Model2 should implement a common interface.
My test class:
import com.fasterxml.jackson.databind.ObjectMapper; public class Tester { static ObjectMapper mapper=new ObjectMapper(); public static void main(String[] args) throws IOException { Car car = new Car(); car.setModel("sedan"); String jsonCar=mapper.writeValueAsString(car); System.out.println(jsonCar); Vehicle c=mapper.readValue(jsonCar, Vehicle.class); System.out.println("Vehicle of type: "+c.getClass().getName()); Truck truck=new Truck(); truck.setPower(100); String jsonTruck=mapper.writeValueAsString(truck); System.out.println(jsonTruck); Vehicle t=mapper.readValue(jsonTruck, Vehicle.class); System.out.println("Vehicle of type: "+t.getClass().getName()); } }
Somewhere you will need to store a mapping between the value of the type field and the corresponding class. Depending on the location where you want this the implementation is different.
1) The parent type holds the list of subtypes:
import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonSubTypes({ @JsonSubTypes.Type(value = Car.class, name = "car"), @JsonSubTypes.Type(value = Truck.class, name = "truck") } ) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") public interface Vehicle { }
The models for Car and Truck are simple POJO without any annotations:
public class Car implements Vehicle { private String model; public String getModel() { return model; } public void setModel(String model) { this.model = model; } }
2) A separate resolver holds the mapping:
Vehicle contains the extra annotation @JsonTypeIdResolver
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") @JsonTypeIdResolver(JsonResolver.class) public interface Vehicle { }
The JsonResolver class holds the mapping between the type field value and the class:
import java.util.HashMap; import java.util.Map; import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; import com.fasterxml.jackson.databind.DatabindContext; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase; public class JsonResolver extends TypeIdResolverBase { private static Map<String,Class<?>> ID_TO_TYPE=new HashMap<>(); static { ID_TO_TYPE.put("car",Car.class); ID_TO_TYPE.put("truck",Truck.class); } public JsonResolver() { super(); } @Override public Id getMechanism() { return null; } @Override public String idFromValue(Object value) { return value.getClass().getSimpleName(); } @Override public String idFromValueAndType(Object value, Class<?> arg1) { return idFromValue(value); } @Override public JavaType typeFromId(DatabindContext context, String id) { return context.getTypeFactory().constructType(ID_TO_TYPE.get(id)); } }
3) The json contains the full class name:
If you accept your serialized json holds the full java class name, you don’t need a resolver but specify use = JsonTypeInfo.Id.CLASS
:
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver; @JsonTypeInfo( use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "type") public interface Vehicle { }
Solution 3 is the easiest to implement but personally I don’t like the idea to have full java class names in my data. It can be a potential risk if you start to refactor your java packages.