Jackson deserialize based on type

后端 未结 4 1615
夕颜
夕颜 2020-12-15 22:09

Lets say I have JSON of the following format:

{
    \"type\" : \"Foo\"
    \"data\" : {
        \"object\" : {
            \"id\" : \"1\"
            \"fizz\         


        
相关标签:
4条回答
  • 2020-12-15 22:45

    I think it is rather straight-forward. You probably have a super class that has properties for metadata and owner, so rather than making it truly generic, you could substitute T for your super class. But basically, you will have to parse the name of the class from the actual JSON string, which in your example would look something like this:

    int start = jsonString.indexOf("type");
    int end = jsonString.indexOf("data");
    Class actualClass = Class.forName(jsonString.substring(start + 4, end - 2)); // that of course, is approximate - based on how you format JSON
    

    and overall code could be something like this:

    public static <T> T deserialize(String xml, Object obj)
            throws JAXBException {
    
        T result = null;
    
        try {
    
            int start = jsonString.indexOf("type");
            int end = jsonString.indexOf("data");
            Class actualClass = Class.forName(jsonString.substring(start + 4, end - 2)); 
    
            JAXBContextFactory factory = JAXBContextFactory.getInstance();
            JAXBContext jaxbContext = factory.getJaxBContext(actualClass);
    
            Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller();
    
            // this will create Java object
            try (StringReader reader = new StringReader(xml)) {
                result = (T) jaxbUnmarshaller.unmarshal(reader);
            }
    
        } catch (JAXBException e) {
            log.error(String
                    .format("Exception while deserialising the object[JAXBException] %s\n\r%s",
                            e.getMessage()));
        }
    
        return result;
    }
    
    0 讨论(0)
  • 2020-12-15 22:47

    Custom deserializer approach

    You could use a custom deserializer that checks the type property to parse the object property into the most suitable class.

    First define an interface that will be implemented by Foo and Bar classes:

    public interface Model {
    
    }
    
    public class Foo implements Model {
    
        // Fields, getters and setters
    }
    
    public class Bar implements Model {
    
        // Fields, getters and setters
    }
    

    Then define your Wrapper and Data classes:

    public class Wrapper {
    
        private String type;
    
        private Data data;
    
        // Getters and setters
    }
    
    public class Data {
    
        @JsonDeserialize(using = ModelDeserializer.class)
        private Model object;
    
        private Metadata metadata;
    
        private Owner owner;
    
        // Getters and setters
    }
    

    The object field is annotated with @JsonDeserialize, indicating the deserializer that will be used for the object property.

    The deserializer is defined as following:

    public class ModelDeserializer extends JsonDeserializer<Model> {
    
        @Override
        public Model deserialize(JsonParser jp, DeserializationContext ctxt)
            throws IOException, JsonMappingException {
    
            // Get reference to ObjectCodec
            ObjectCodec codec = jp.getCodec();
    
            // Parse "object" node into Jackson's tree model
            JsonNode node = codec.readTree(jp);
    
            // Get value of the "type" property
            String type = ((Wrapper) jp.getParsingContext().getParent()
                .getCurrentValue()).getType();
    
            // Check the "type" property and map "object" to the suitable class
            switch (type) {
    
                case "Foo":
                    return codec.treeToValue(node, Foo.class);
    
                case "Bar":
                    return codec.treeToValue(node, Bar.class);
    
                default:
                    throw new JsonMappingException(jp, 
                        "Invalid value for the \"type\" property");
            }
        }
    }
    

    The JSON document can be deserialized as following:

    ObjectMapper mapper = new ObjectMapper();
    Wrapper wrapper = mapper.readValue(json, Wrapper.class);  
    

    Alternatively to this custom deserializer, consider an annotations-only approach.

    0 讨论(0)
  • 2020-12-15 22:58

    All this can be done by means of annotations.

    Create an abstract superclass with the common fields like "metadata" and "owner" and their getters/setters. This class needs to be annotated with @JsonTypeInfo. It should look like:

    @JsonTypeInfo(use = Id.CLASS, include = As.PROPERTY, property = "type")
    

    With the parameter property = "type" you specify that the class identifier will be serialized under the field type in your JSON document.

    The value of the class identifier can be specified with use. Id.CLASS uses the fully-qualified Java class name. You can also use Id.MINIMAL_CLASS which is an abbreviated Java class name. To have your own identifier, use Id.NAME. In this case, you need to declare the subtypes:

    @JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "type")
    @JsonSubTypes({
        @JsonSubTypes.Type(value = Foo.class, name = "Foo"),
        @JsonSubTypes.Type(value = Bar.class, name = "Bar")
    })
    

    Implement your classes Foo and Bar by extending from the abstract superclass.

    Jackson's ObjectMapper will use the additional field "type" of the JSON document for serialization and deserialization. E. g. when you deserialise a JSON string into a super class reference, it will be of the appropriate subclass:

    ObjectMapper om = new ObjectMapper();
    AbstractBase x = om.readValue(json, AbstractBase.class);
    // x will be instanceof Foo or Bar
    


    Complete code example (I used public fields as shortcut to not need to write getters/setters):

    package test;
    
    import com.fasterxml.jackson.annotation.JsonTypeInfo;
    import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
    
    import java.io.IOException;
    
    import com.fasterxml.jackson.annotation.JsonSubTypes;
    
    @JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "type")
    @JsonSubTypes({
        @JsonSubTypes.Type(value = Foo.class, name = "Foo"),
        @JsonSubTypes.Type(value = Bar.class, name = "Bar")
    })
    public abstract class AbstractBase {
    
        public MetaData metaData;
        public Owner owner;
        @Override
        public String toString() {
            return "metaData=" + metaData + "; owner=" + owner;
        }
    
        public static void main(String[] args) throws IOException {
    
            // Common fields
            Owner owner = new Owner();
            owner.name = "Richard";
            MetaData metaData = new MetaData();
            metaData.data = "Some data";
    
            // Foo
            Foo foo = new Foo();
            foo.owner = owner;
            foo.metaData = metaData;
            CustomObject customObject = new CustomObject();
            customObject.id = 20l;
            customObject.fizz = "Example";
            Data data = new Data();
            data.object = customObject;
            foo.data = data;
            System.out.println("Foo: " + foo);
    
            // Bar
            Bar bar = new Bar();
            bar.owner = owner;
            bar.metaData = metaData;
            bar.data = "A String in Bar";
    
            ObjectMapper om = new ObjectMapper();
    
            // Test Foo:
            String foojson = om.writeValueAsString(foo);
            System.out.println(foojson);
            AbstractBase fooDeserialised = om.readValue(foojson, AbstractBase.class);
            System.out.println(fooDeserialised);
    
            // Test Bar:
            String barjson = om.writeValueAsString(bar);
            System.out.println(barjson);
            AbstractBase barDeserialised = om.readValue(barjson, AbstractBase.class);
            System.out.println(barDeserialised);
    
        }
    
    }
    
    class Foo extends AbstractBase {
        public Data data;
        @Override
        public String toString() {
            return "Foo[" + super.toString() + "; data=" + data + ']';
        }
    }
    
    class Bar extends AbstractBase {
        public String data;
        public String toString() {
            return "Bar[" + super.toString() + "; data=" + data + ']';
        }
    }
    
    
    class Data {
        public CustomObject object;
        @Override
        public String toString() {
            return "Data[object=" + object + ']';
        }
    }
    
    class CustomObject {
        public long id;
        public String fizz;
        @Override
        public String toString() {
            return "CustomObject[id=" + id + "; fizz=" + fizz + ']';
        }
    }
    
    class MetaData {
        public String data;
        @Override
        public String toString() {
            return "MetaData[data=" + data + ']';
        }
    }
    
    class Owner {
        public String name;
        @Override
        public String toString() {
            return "Owner[name=" + name + ']';
        }
    }
    
    0 讨论(0)
  • 2020-12-15 23:09

    Annotations-only approach

    Alternatively to the custom deserializer approach, you can have the following for an annotations-only solution (similar to the one described in Spunc's answer, but using type as an external property):

    public abstract class AbstractData {
    
        private Owner owner;
    
        private Metadata metadata;
    
        // Getters and setters
    }
    
    public static final class FooData extends AbstractData {
    
        private Foo object;
    
        // Getters and setters
    }
    
    public static final class BarData extends AbstractData {
    
        private Bar object;
    
        // Getters and setters
    }
    
    public class Wrapper {
    
        private String type;
    
        @JsonTypeInfo(use = Id.NAME, property = "type", include = As.EXTERNAL_PROPERTY)
        @JsonSubTypes(value = { 
                @JsonSubTypes.Type(value = FooData.class, name = "Foo"),
                @JsonSubTypes.Type(value = BarData.class, name = "Bar") 
        })
        private AbstractData data;
    
        // Getters and setters
    }
    

    In this approach, @JsonTypeInfo is set to use type as an external property to determine the right class to map the data property.

    The JSON document can be deserialized as following:

    ObjectMapper mapper = new ObjectMapper();
    Wrapper wrapper = mapper.readValue(json, Wrapper.class);  
    
    0 讨论(0)
提交回复
热议问题