Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamically choosing which class to create object of from json

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.

like image 272
b15 Avatar asked Jul 18 '19 20:07

b15


2 Answers

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.

like image 108
Conffusion Avatar answered Oct 12 '22 07:10

Conffusion


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:

  • it doesn't return anything as you would have to have another visitor pattern and implement a receiver for each re-serialized (as you called it) object
  • you still have to implement / find a library that reads type hints from the received json (like the code comment states, jackson can do this)

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.

like image 43
roookeee Avatar answered Oct 12 '22 07:10

roookeee