jax
jax

Reputation: 38643

How to create a general JsonDeserializer

I need to create a general deserializer; in other words I don't know what the deserialised target class will be.

I have seen examples on the internet where by they create a deserializer such as JsonDeserializer<Customer> and then return a new Customer(...) at the end. The problem is that I don't know what the return class will be.

I imagine I will need to use reflection to create an instance of the class and populate the field. How can I do it from the deserialize method?

public class JsonApiDeserializer extends JsonDeserializer<Object> {

    @Override
    public Object deserialize(JsonParser jp, DeserializationContext ctxt)
            throws IOException, JsonProcessingException {

        //Need to parse the JSON and return a new instance here
    }

}

Upvotes: 4

Views: 7574

Answers (6)

PickBoy
PickBoy

Reputation: 1314

After some tests, I find @jax 's answer has a problem.

As @Staxman pointed out, createContextual() is called during construction of Deserializer, not in every process of deserialization. And the deserializer returned by createContextual will be cached by the Jackson library. So if your deserializer is used with more than 1 type(such as sub types of a common parent), it will throw out type mismatch exception, cause the targetClass property will be the last type cached by the Jackson library.

The correct solution should be:

public class JsonApiDeserializer extends JsonDeserializer<Object> implements
        ContextualDeserializer {

    private Class<?> targetClass;

    public JsonApiDeserializer() {
    }

    public JsonApiDeserializer(Class<?> targetClass) {
        this.targetClass = targetClass;
    }

    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt)
            throws IOException, JsonProcessingException {
        Object clazz = targetClass.newInstance();
        //Now I have an instance of the annotated class I can populate the fields via reflection
        return clazz;
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
            BeanProperty property) throws JsonMappingException {
        //gets the class type of the annotated class
        targetClass = ctxt.getContextualType().getRawClass();
        //this new JsonApiDeserializer will be cached
        return new JsonApiDeserializer(targetClass);
    }
}

Upvotes: 4

jax
jax

Reputation: 38643

I got it working using ContextualDeserializer

public class JsonApiDeserializer extends JsonDeserializer<Object> implements
        ContextualDeserializer {

    private Class<?> targetClass;

    @SneakyThrows
    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt)
            throws IOException, JsonProcessingException {
        Object clazz = targetClass.newInstance();

        //Now I have an instance of the annotated class I can populate the fields via reflection

        return clazz;
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
            BeanProperty property) throws JsonMappingException {
        //gets the class type of the annotated class
        targetClass = ctxt.getContextualType().getRawClass();
        return this;
    }

}

I am still a little unsure of why this works as I already have a DeserializationContext ctxt in the original deserialize method but it returns null when I do ctxt.getContextualType().

Can someone explain?

Upvotes: 1

Bohemian
Bohemian

Reputation: 425348

Essentially, there are only 2 cases you need to cater for, Object and Object[], for which you can always deserialize to:

  1. A Map
  2. An array of Map

Something like this should work:

public class JsonApiDeserializer extends JsonDeserializer<Object> {
    @Override
    public Object deserialize(JsonParser jp, DeserializationContext ctxt)
            throws IOException, JsonProcessingException {
        String text = jp.getText();
        if (text.startsWith("{"))
            return new ObjectMapper().readValue(text, Map.class);
        return new ObjectMapper().readValue(text, Map[].class);
    }
}

Disclaimer: Uncompiled and untested

Upvotes: 1

Alexander Goncharenko
Alexander Goncharenko

Reputation: 54

If you know which classes can be deserialized in compile-time, but need to dynamically choose the right one in runtime depending on JSON contents I can suggest the following.

  1. Add some classifier field into the JSON. This field will help your code know how to deal with the following data. As far as I can see, you already have the "type" field so that can be used.
  2. Introduce a factory that will instantiate the specific classes depending on the input from JSON. For example, it may have the method like Object create(string typeFromJson, Map data). Such factory may populate the newly-created object with the data as well.

If this is not the case and you don't know your required interfaces already, you are in trouble. It can be somewhat be worked around in C# with the use of dynamic keyword, but Java doesn't have such a feature yet.

Also, AFAIK, there is a way in Jackson to specify classes that need to be automatically deserialized and injected into @Post method calls in your REST resource class.

Upvotes: 0

Emanuele Ivaldi
Emanuele Ivaldi

Reputation: 1332

I am not sure I completely got your question right but what you can do is to inspect the properties of the json inside the deserialiser doing something like:


ObjectMapper objectMapper = (ObjectMapper) jp.getCodec();
ObjectNode node = objectMapper.readTree(jp);
and then node.has("propertyName") so that you can create, setup and return your object and leave to the client of the deserialiser the responsibility of the cast.

you say " in other words I don't know what the deserialised target class will be. "

so I don't get if you can at least infer that, more info would be helpful

Upvotes: 0

Amila
Amila

Reputation: 5213

If you know the message structure in advance, you can use this tool to easily generate POJOs from a given JSON string.

However, if your message format changes during runtime, and there is no other information for you to determine the type (for example, header information) you can deserialize into a Map and process the fields manually.

For example, with Jackson:

ObjectMapper mapper = new ObjectMapper();
Map<String, Object> userData = mapper.readValue(jsonData, Map.class);

Upvotes: 0

Related Questions