How use jackson ObjectMapper inside custom deserializer?

Cherry picture Cherry · Sep 13, 2015 · Viewed 13.5k times · Source

I try to write custom jackson deserializer. I want "look" at one field and perform auto deserialization to class, see example below:

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.mypackage.MyInterface;
import com.mypackage.MyFailure;
import com.mypackage.MySuccess;

import java.io.IOException;

public class MyDeserializer extends JsonDeserializer<MyInterface> {

    @Override
    public MyInterface deserialize(JsonParser jp, DeserializationContext ctxt)
            throws IOException, JsonProcessingException {
        ObjectCodec codec = jp.getCodec();
        JsonNode node = codec.readTree(jp);
        if (node.has("custom_field")) {
            return codec.treeToValue(node, MyFailure.class);
        } else {
            return codec.treeToValue(node, MySuccess.class);
        }
    }
}

Pojos:

public class MyFailure implements MyInterface {}

public class MySuccess implements MyInterface {}

@JsonDeserialize(using = MyDeserializer.class)
public interface MyInterface {}

And I got StackOverflowError. In understand that codec.treeToValue call same deserializer. Is there a way to use codec.treeToValue or ObjectMapper.readValue(String,Class<T>) inside custome deseralizer?

Answer

araqnid picture araqnid · Sep 14, 2015

The immediate problem seems to be that the @JsonDeserialize(using=...) is being picked up for your implementations of MyInterface as well as MyInterface itself: hence the endless loop.

You can fix this my overriding the setting in each implementation:

@JsonDeserialize(using=JsonDeserializer.None.class)
public static class MySuccess implements MyInterface {
}

Or by using a module instead of an annotation to configure the deserialization (and removing the annotation from MyInterface):

mapper.registerModule(new SimpleModule() {{
    addDeserializer(MyInterface.class, new MyDeserializer());
}});

On a side-note, you might also consider extending StdNodeBasedDeserializer to implement deserialization based on JsonNode. For example:

@Override
public MyInterface convert(JsonNode root, DeserializationContext ctxt) throws IOException {
    java.lang.reflect.Type targetType;
    if (root.has("custom_field")) {
        targetType = MyFailure.class;
    } else {
        targetType = MySuccess.class;
    }
    JavaType jacksonType = ctxt.getTypeFactory().constructType(targetType);
    JsonDeserializer<?> deserializer = ctxt.findRootValueDeserializer(jacksonType);
    JsonParser nodeParser = root.traverse(ctxt.getParser().getCodec());
    nodeParser.nextToken();
    return (MyInterface) deserializer.deserialize(nodeParser, ctxt);
}

There are a bunch of improvements to make to this custom deserializer, especially regarding tracking the context of the deserialization etc., but this should provide the functionality you're asking for.