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.
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.
You can make use of the visitor pattern here:
class ScratchStackOverflowQuestion57102092 {
interface Reserializer {
void accept(Model1 model1);
void accept(Model2 model2);
}
interface Reserializeable {
void visit(Reserializer reserializer);
}
class Model1 implements Reserializeable {
@Override
public void visit(Reserializer reserializer) {
reserializer.accept(this);
}
}
class Model2 implements Reserializeable {
@Override
public void visit(Reserializer reserializer) {
reserializer.accept(this);
}
}
public class ReserializerImpl implements Reserializer {
@Override
public void accept(Model1 model1) {
//TODO: reserialize and push the new object somewhere
}
@Override
public void accept(Model2 model2) {
//TODO: reserialize and push the new object somewhere
}
}
public class JsonConversion {
//TODO: instantiate etc
private Reserializer reserializer;
public void handleJson(String json) {
//TODO: use some framework like Jackson which can read type hints from JSON fields
Reserializeable serialized = mapFromJson(json);
serialized.visit(reserializer);
}
}
}
This is a simplified example on how to accomplish what you want but it lacks the following features for now:
So you may have to adapt the given code a little :)
EDIT: Due to popular demand a fully fledged implementation which enables dynamic handling ob re-serialized objects via another visitor (it only lacks the Jackson usage that considers a type hint in the JSON). I apologize for the nearly dozen classes, there is just no shorter way. See function exampleUsage()
on how this approach is used / how to define handlers for the different reconverted objects:
class ScratchStackOverflowQuestion57102092_V2 {
//////////////////////////////// INPUTS //////////////////////////////
interface Reserializer {
void accept(Model1 model1);
void accept(Model2 model2);
}
interface Reserializeable {
void visit(Reserializer reserializer);
}
class Model1 implements Reserializeable {
@Override
public void visit(Reserializer reserializer) {
reserializer.accept(this);
}
}
class Model2 implements Reserializeable {
@Override
public void visit(Reserializer reserializer) {
reserializer.accept(this);
}
}
//////////////////////////////// RECONVERSION /////////////////////////
interface ReconvertedVisitor {
void accept(ReconvertedModel1 reconverted);
void accept(ReconvertedModel2 reconverted);
}
interface ReconvertedModel {
void visit(ReconvertedVisitor visitor);
}
//Some dummy object as an example
class ReconvertedModel1 implements ReconvertedModel{
@Override
public void visit(ReconvertedVisitor visitor) {
visitor.accept(this);
}
}
//Some dummy object as an example
class ReconvertedModel2 implements ReconvertedModel{
@Override
public void visit(ReconvertedVisitor visitor) {
visitor.accept(this);
}
}
////////////////////////////// IMPLEMENTATIONS ///////////////////////////////
public class ReserializerImpl implements Reserializer {
private final ReconvertedVisitor visitor;
public ReserializerImpl(ReconvertedVisitor visitor) {
this.visitor = visitor;
}
@Override
public void accept(Model1 model1) {
//TODO: do some real conversion
ReconvertedModel1 reserializeResult = new ReconvertedModel1();
}
@Override
public void accept(Model2 model2) {
//TODO: do some real conversion
ReconvertedModel2 reserializeResult = new ReconvertedModel2();
}
}
public class JsonConversion {
public void handleJson(String json, ReconvertedVisitor handler) {
//TODO: use some framework like Jackson which can read type hints from JSON fields
Reserializeable serialized = mapFromJson(json);
ReserializerImpl reserializer = new ReserializerImpl(handler);
serialized.visit(reserializer);
}
}
public void exampleUsage() {
//Just some sample, you could delegate to different objects in each accept
class PrintingReconvertedVisitor implements ReconvertedVisitor {
@Override
public void accept(ReconvertedModel1 reconverted) {
System.out.println(reconverted);
}
@Override
public void accept(ReconvertedModel2 reconverted) {
System.out.println(reconverted);
}
}
JsonConversion conversion = new JsonConversion();
conversion.handleJson("TODO: SOME REAL JSON HERE", new PrintingReconvertedVisitor());
}
}
I am not too happy with the naming of the classes, maybe rename Reserializer
so ModelVisitor
or something appropriate.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With