Reputation: 33544
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?
Upvotes: 29
Views: 30574
Reputation: 133482
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 by 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.
Upvotes: 23
Reputation: 1803
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
}
}
}
Upvotes: 0
Reputation: 181
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
}
}
Upvotes: 0
Reputation: 3888
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
}
}
Upvotes: 1