27

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?

Cœur
  • 37,241
  • 25
  • 195
  • 267
Cherry
  • 31,309
  • 66
  • 224
  • 364

5 Answers5

21

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.

araqnid
  • 127,052
  • 24
  • 157
  • 134
  • I've tried extending StdNodeBasedDeserializer but I'm asked to provide no-args constructor. And if I do that the _treeDeserializer field is null... – mvmn Jul 03 '19 at 12:52
  • @araqnid can you elaborate on the improvements that you mentioned? – Sergey Apr 19 '20 at 12:04
1

In order to use your own ObjectMapper inside a custom deserializer, you can use Jackson Mix-in Annotations (the DefaultJsonDeserializer interface) to dynamically remove the custom deserializer from the POJO classes, avoiding the StackOverflowError that would otherwise be thrown as a result of objectMapper.readValue(JsonParser, Class<T>).

public class MyDeserializer extends JsonDeserializer<MyInterface> {

    private static final ObjectMapper objectMapper = new ObjectMapper();

    static {
        objectMapper.addMixIn(MySuccess.class, DefaultJsonDeserializer.class);
        objectMapper.addMixIn(MyFailure.class, DefaultJsonDeserializer.class);
    }

    @Override
    public MyInterface deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        if (jp.getCodec().<JsonNode>readTree(jp).has("custom_field")) {
            return objectMapper.readValue(jp, MyFailure.class);
        } else {
            return objectMapper.readValue(jp, MySuccess.class);
        }           
    }

    @JsonDeserialize
    private interface DefaultJsonDeserializer {
        // Reset default json deserializer
    }

}
lcnicolau
  • 3,252
  • 4
  • 36
  • 53
0

I find a solution to use object mapper inside custom deserialize

public class DummyDeserializer extends JsonDeserializer<Dummy> {

    @Override
    public Dummy deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
        ObjectMapper om = new ObjectMapper();
        om.addMixIn(NexusAccount.class, DefaultJsonDeserializer.class);
        ObjectCodec oc = jsonParser.getCodec();
        JsonNode node = oc.readTree(jsonParser);
        String serviceType = node.path("serviceType").asText();
        switch (serviceType) {
            case "Dummy1":
                return om.treeToValue(node, Dumm1.class);
            case "Dummy2":
                return om.treeToValue(node, Dummy2.class);
            case "Dummy3":
                return om.treeToValue(node, Dummy3.class);
            default:
                throw new IllegalArgumentException("Unknown Dummy type");
        }
    }
 
    @JsonDeserialize
    private interface DefaultJsonDeserializer {
        // Reset default json deserializer
    }
}
Shubham Kumar
  • 121
  • 1
  • 4
0
Sharing a custom Deserializer implementing--->

    import com.fasterxml.jackson.core.JsonParser;
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.DeserializationContext;
    import com.fasterxml.jackson.databind.JsonDeserializer;
    import com.fasterxml.jackson.databind.JsonMappingException;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.HttpStatus;
    
    import java.io.IOException;
    
    public class CustomIntegerDeserializer extends JsonDeserializer<Integer> {
    
        private static Logger logger = LoggerFactory.getLogger(CustomIntegerDeserializer.class);
    
        @Override
        public Integer deserialize(JsonParser p, DeserializationContext ctx)
             throws IOException,JsonProcessingException {
            logger.info("CustomIntegerDeserializer..............................");
            Double doubleVal = p.getDoubleValue();
            try {
                double ceil = Math.ceil(doubleVal);
                double floor = Math.floor(doubleVal);
                if((ceil-floor)>0) {
                    String message = String.format("Cannot coerce a floating-point value ('%s') into Integer", doubleVal.toString());
                    throw new Exception(message);
                }
                return Integer.valueOf(p.getIntValue());
            } catch (Exception exception) {
                logger.info("Exception in CustomIntegerDeserializer::{}",exception.getMessage());
          
                //throw new custom exception which are to be managed by exception handler
                
            }
        }
    
    }
abhinav kumar
  • 1,487
  • 1
  • 12
  • 20
-4

This did the trick for me:

ctxt.readValue(node, MyFailure.class)
Julio
  • 540
  • 1
  • 3
  • 10
  • This doesn't make any sense, that `readValue` method on DeserializationContext has the following signature : `public T readValue(JsonParser p, Class type)` – singe3 Oct 28 '21 at 15:33