Using Jackson, is there a way to deserialize a proprty that depends on the value of another property?
if i have this json {"foo":"a","bar":"b"} i'd like to deserialize it to the Test class below as Test [foo=a, bar=b_a], where bar is the value of the json property "bar" and the value of the property "foo".
Of course this is a trivial example, the real deal would be to deserialize a datamodel entity: {"line":"C12", "machine": {"line":"C12", "code":"A"}} machine.line and line are always the same, and i'd like to express it like this: {"line":"C12", "machine": "A"}
import java.io.IOException;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
public abstract class Main{
private static class Test {
#JsonProperty
private String foo;
#JsonProperty
#JsonDeserialize(using = CustomDeserializer.class)
private String bar;
// ...other fields to be deserialized with default behaviour
private Test() {
}
public Test(String a, String bar) {
this.foo = a;
this.bar = bar;
}
#Override
public String toString() {
return "Test [foo=" + foo + ", bar=" + bar + "]";
}
}
private static class CustomDeserializer extends StdDeserializer<String> {
protected CustomDeserializer() {
super(String.class);
}
#Override
public String deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
String foo = //how to get foo property?
String value = p.getValueAsString();
if (!foo.isEmpty()) {
return value + "_" + foo;
} else {
return value;
}
}
}
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
Test foo2 = mapper.readValue("{\"foo\":\"a\",\"bar\":\"b\"}", Test.class);
System.out.println(foo2); // Test [foo=a, bar=b_a]
}
}
One way to solve your problem is specify a custom deserializer that involves your Test class instead of your string field because the deserialization of your property is based on the value of another property:
public class CustomDeserializer extends JsonDeserializer<Test> {}
#JsonDeserialize(using = CustomDeserializer.class)
public class Test {}
Then you can deserialize your object reading the JsonNode tree built from your input string:
public class CustomDeserializer extends JsonDeserializer<Test> {
#Override
public Test deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
String foo = node.get("foo").asText();
String bar = node.get("bar").asText();
if (!foo.isEmpty()) {
bar = (bar + '_' + foo);
}
return new Test(foo, bar);
}
}
//your example
public class Main {
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
Test foo2 = mapper.readValue("{\"foo\":\"a\",\"bar\":\"b\"}", Test.class);
System.out.println(foo2); // Test [foo=a, bar=b_a]
}
}
I got a similar problem today and I wanted to share my solution. So instead of using a #JsonDeserialize, I use a #JsonCreator on the parent object with a package private constructor to accept the "raw" properties and then I can process this data and return better objects.
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
class Scratch {
public static void main(String[] args) throws Exception{
final var testData = "{\"foo\":\"a\",\"bar\":\"b\"}";
final var mapper = new ObjectMapper();
final var testObj = mapper.readValue(testData, Test.class);
System.out.println(testObj); // Test[foo=a, bar=a_b]
}
record Test (
String foo,
String bar
){
#JsonCreator Test(
#JsonProperty("foo") String foo,
#JsonProperty("bar") String bar,
#JsonProperty("_dummy") String _dummy // extra param for the constructor overloading
) {
this(foo, deserializeBar(foo, bar));
}
private static String deserializeBar(String foo, String bar) {
if (foo == null || foo.isEmpty()) {
return bar;
}
return "%s_%s".formatted(foo, bar);
}
}
}
In the end, I've resorted using BeanDeserializerModifier
Please notice that the following code is not fully functioning because it relies on code I'm not allowed to share, but it should suffice to get the idea.
package com.example;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.reflections.Reflections;
import org.reflections.scanners.SubTypesScanner;
import org.reflections.scanners.TypeAnnotationsScanner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.deser.BeanDeserializer;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
public class JsonDelegateDeserializerModule extends SimpleModule {
// !! must be registered as guice factory
public interface JsonDelegateDeserializerFactory {
JsonDelegateDeserializerModule create(String packagePath);
}
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
public #interface JsonDelegateDeserializer {
public Class<? extends StdDeserializer<?>> deserializer();
public Class<?> forType();
}
protected interface JsonDeserializerFactory {
// non metto nessun generic in TagHandler o guice non riesce piu a creare la
// factory!
#SuppressWarnings("rawtypes")
public JsonDeserializer create(JsonDeserializer baseDeserializer);
}
private static final Logger LOGGER = LoggerFactory.getLogger(JsonDelegateDeserializerModule.class);
#Inject
private FactoryInjector injector;
private final String packagePath;
#AssistedInject
protected JsonDelegateDeserializerModule(#Assisted String packagePath) {
super();
this.packagePath = packagePath;
}
#Override
public String getModuleName() {
return JsonDelegateDeserializerModule.class.getSimpleName() + "[" + packagePath + "]";
}
#Override
public Object getTypeId() {
return JsonDelegateDeserializerModule.class.getSimpleName() + "[" + packagePath + "]";
}
#Override
public void setupModule(SetupContext context) {
Reflections reflectios = new Reflections(packagePath, new SubTypesScanner(), new TypeAnnotationsScanner());
Map<Class<?>, JsonDeserializerFactory> classToDeserializerFactory = new HashMap<>();
Set<Class<?>> classesWithModifier = reflectios.getTypesAnnotatedWith(JsonDelegateDeserializer.class);
for (Class<?> classWithModifier : classesWithModifier) {
JsonDelegateDeserializer annotation = classWithModifier.getAnnotation(JsonDelegateDeserializer.class);
if (annotation != null) {
Class<? extends StdDeserializer<?>> deserializerType = annotation.deserializer();
Class<?> forType = annotation.forType();
try {
JsonDeserializerFactory factory = injector.getFactory(JsonDeserializerFactory.class,
deserializerType);
classToDeserializerFactory.put(forType, factory);
} catch (Exception e) {
LOGGER.error("Exception was thown while creating deserializer {} for type {}:", deserializerType,
forType, e);
throw new RuntimeException(e);
}
}
}
if (!classToDeserializerFactory.isEmpty()) {
setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc,
JsonDeserializer<?> deserializer) {
List<Class<?>> possibleTypesList = new LinkedList<>();
if (deserializer instanceof BeanDeserializer) {
for (Entry<Class<?>, JsonDeserializerFactory> entry : classToDeserializerFactory.entrySet()) {
Class<?> type = entry.getKey();
if (type.isAssignableFrom(deserializer.handledType())) {
possibleTypesList.add(type);
}
}
if (possibleTypesList.size() > 1) {
possibleTypesList.sort(new Comparator<Class<?>>() {
#Override
public int compare(Class<?> o1, Class<?> o2) {
if (o1.isAssignableFrom(o2)) {
return 1;
} else {
return -1;
}
}
});
}
Class<?> type = Utils.first(possibleTypesList);
if (type == null) {
return super.modifyDeserializer(config, beanDesc, deserializer);
} else {
JsonDeserializerFactory factory = classToDeserializerFactory.get(type);
JsonDeserializer<?> modifiedDeserializer = factory.create(deserializer);
return super.modifyDeserializer(config, beanDesc, modifiedDeserializer);
}
} else {
// รจ gia stato impostato un deserializzatore piu specifico, non imposato questo
return super.modifyDeserializer(config, beanDesc, deserializer);
}
}
});
}
super.setupModule(context);
}
}
then you can simply annotate the Mixin to add the custom deserializer
#JsonDelegateDeserializer(deserializer = LoadLineDeserializer.class, forType = Line.class)
public interface LineMixIn {
public static class LoadLineDeserializer extends DelegatingDeserializer {
#AssistedInject
public LoadLineDeserializer(#Assisted JsonDeserializer baseDeserializer, LineService lineService) {
super(baseDeserializer);
}
// ...
}
// ...
}
Related
I am trying to read a YAML file and store the result in a list of POJOs.
I cannot modify the YAML file. I use Jackson 2.10.0 but I am open to any other version. I am trying to parse the following script with Jackson:
vehicles-notype.yaml
Vehicles is basically a list of objects with some common properties and some unique to the type of vehicle.
---
vehicles:
- car:
make: "Mercedes-Benz"
model: "S500"
topSpeed: 250.0
seatingCapacity: 5
- truck:
make: "Isuzu"
model: "NQR"
payloadCapacity: 7500.0
Desired outpout
After reading the file, I'd like that, if I introspect the list, I'd like to get:
... App.java:48): -> start()
... App.java:56): class net.jgp.labs.jackson.yaml.lab411_pojos.Car
... App.java:56): class net.jgp.labs.jackson.yaml.lab411_pojos.Truck
The Car and Truck POJOs are pretty obvious:
Car
package net.jgp.labs.jackson.yaml.lab411_pojos;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class Car extends Vehicle {
private int seatingCapacity;
private double topSpeed;
#JsonCreator
public Car(
#JsonProperty("make") String make,
#JsonProperty("model") String model,
#JsonProperty("seating") int seatingCapacity,
#JsonProperty("topSpeed") double topSpeed) {
super(make, model);
this.seatingCapacity = seatingCapacity;
this.topSpeed = topSpeed;
}
public int getSeatingCapacity() {
return seatingCapacity;
}
public void setSeatingCapacity(int seatingCapacity) {
this.seatingCapacity = seatingCapacity;
}
public double getTopSpeed() {
return topSpeed;
}
public void setTopSpeed(double topSpeed) {
this.topSpeed = topSpeed;
}
public String getType() {
return "car";
}
}
Truck
package net.jgp.labs.jackson.yaml.lab411_pojos;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class Truck extends Vehicle {
private double payloadCapacity;
#JsonCreator
public Truck(
#JsonProperty("make") String make,
#JsonProperty("model") String model,
#JsonProperty("payload") double payloadCapacity) {
super(make, model);
this.payloadCapacity = payloadCapacity;
}
public double getPayloadCapacity() {
return payloadCapacity;
}
public void setPayloadCapacity(double payloadCapacity) {
this.payloadCapacity = payloadCapacity;
}
#Override
public String getType() {
return "truck";
}
}
Fleet
The Fleet POJO is also obvious.
package net.jgp.labs.jackson.yaml.lab411_pojos;
import java.util.List;
public class Fleet {
private List<Vehicle> vehicles;
public void setVehicles(List<Vehicle> vehicles) {
this.vehicles= vehicles;
}
public List<Vehicle> getVehicles() {
return vehicles;
}
}
Vehicle
Vehicle is a bit more tricky, as I am trying to play with #JsonTypeInfo and #JsonSubTypes. You can see the commented code, which is slowly driving me mad:
package net.jgp.labs.jackson.yaml.lab411_pojos;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
#JsonTypeInfo(
use = JsonTypeInfo.Id.CLASS,
include = JsonTypeInfo.As.EXTERNAL_PROPERTY
// ,
// property = "className"
)
#JsonSubTypes({
#Type(value = Car.class, name = "car"),
#Type(value = Truck.class, name = "truck")
})
//#JsonSubTypes({
// #Type(value = Car.class, name = "car"),
// #Type(value = Truck.class, name = "truck")
//})
public abstract class Vehicle {
private String make;
private String model;
#JsonProperty("type")
abstract public String getType();
public void setType(String type) {};
protected Vehicle(String make, String model) {
this.make = make;
this.model = model;
}
public String getMake() {
return make;
}
public void setMake(String make) {
this.make = make;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
}
App
Finally the application code, which is pretty obvious too.
package net.jgp.labs.jackson.yaml.lab411_read_diff_objects;
import java.io.File;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import net.jgp.labs.jackson.yaml.lab411_pojos.Fleet;
import net.jgp.labs.jackson.yaml.lab411_pojos.Vehicle;
/**
* What does it do?
*
* #author jgp
*/
public class ReadListVehicleNoTypeApp {
private static final Logger log =
LoggerFactory.getLogger(ReadListVehicleNoTypeApp.class);
/**
* main() is your entry point to the application.
*
* #param args
*/
public static void main(String[] args) {
ReadListVehicleNoTypeApp app = new ReadListVehicleNoTypeApp();
try {
app.start();
} catch (JsonProcessingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* The processing code.
*
* #throws IOException
*/
protected boolean start() throws IOException {
log.debug("-> start()");
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
Fleet fleet = mapper.readValue(new File("data/vehicles-notype.yaml"),
Fleet.class);
for (Vehicle v : fleet.getVehicles()) {
log.debug("{}", v.getClass());
}
return true;
}
}
I am pretty sure there is something to play with the #Json family of attributes, but I am slowly losing it ;-).
car and truck are a field names, properties. I am not aware about Jackson annotation which allows to set types from different fields.
If Yaml file can not be modified, we can use Streaming API to read type property and deserialise Vehicle. In pseudocode it could look like:
while token != EOF
while token != FIELD_NAME
nextToken()
fieldName = nextFieldName();
clazz = convertToClass(fieldName);
vehicles.add(read(clazz));
Luckily, field name which defines type is a first field name and we can read it manually and use Jackson to read type after that. I removed JsonSubTypes and JsonTypeInfo annotation from Vehicle class and with Streaming API it could look like below:
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLParser;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class YamlApp {
public static void main(String[] args) throws Exception {
File yamlFile = new File("./resource/test.yaml").getAbsoluteFile();
FleetDeserializer deserializer = new FleetDeserializer();
Fleet fleet = deserializer.readValue(yamlFile);
System.out.println(fleet);
}
}
class FleetDeserializer {
private YAMLFactory factory = new YAMLFactory();
private ObjectMapper mapper = new ObjectMapper(factory);
public Fleet readValue(File yamlFile) throws IOException {
Fleet fleet = new Fleet();
fleet.setVehicles(new ArrayList<>());
YAMLParser parser = factory.createParser(yamlFile);
while (parser.nextToken() != null) {
if (parser.getCurrentToken() != JsonToken.START_OBJECT) {
continue;
}
// skip everything until a field name
while (parser.nextToken() != JsonToken.FIELD_NAME) ;
Class<? extends Vehicle> type = getType(parser.getCurrentName());
if (type == null) {
continue;
}
// skip field name
parser.nextToken();
parser.nextToken();
// read next vehicle
fleet.getVehicles().add(mapper.readValue(parser, type));
}
return fleet;
}
private Class<? extends Vehicle> getType(String fieldName) {
Objects.requireNonNull(fieldName);
switch (fieldName) {
case "car":
return Car.class;
case "truck":
return Truck.class;
default:
return null;
}
}
}
Above code prints:
Fleet{vehicles=[Car{seatingCapacity=5, topSpeed=250.0, make='Mercedes-Benz', model='S500'}, Truck{payloadCapacity=7500.0, make='Isuzu', model='NQR'}]}
Lets suppose, that we have a bean like this:
public class Response<T> {
private T data;
private double executionDuration;
private boolean success;
private String version;
//HOW TO Make Jackson to inject this?
private Class<T> dataClass;
public Optional<T> getData() {
return Optional.ofNullable(data);
}
public double getExecutionDuration() {
return executionDuration;
}
public Class<T> getDataClass() {
return dataClass;
}
public String getVersion() {
return version;
}
public boolean isSuccess() {
return success;
}
}
The deserialization happens like this:
objectMapper.readValue(json, new TypeReference<Response<SomeClass>>() {});
Can I somehow make Jackson to inject the class "SomeClass" into my bean? Injecting the type reference itself would be also ok, I think.
If it is undesirable to save class info in json and use #JsonTypeInfo I would suggest to use #JacksonInject:
public class Response<T> {
private T data;
private double executionDuration;
private boolean success;
private String version;
#JacksonInject("dataClass")
private Class<T> dataClass;
public Optional<T> getData() {
return Optional.ofNullable(data);
}
public double getExecutionDuration() {
return executionDuration;
}
public Class<T> getDataClass() {
return dataClass;
}
public String getVersion() {
return version;
}
public boolean isSuccess() {
return success;
}
}
Deserialization would look like:
ObjectMapper mapper = new ObjectMapper();
InjectableValues.Std injectable = new InjectableValues.Std();
injectable.addValue("dataClass", SomeClass.class);
mapper.setInjectableValues(injectable);
final Response<Integer> response = mapper.readValue(json, new TypeReference<Response<SomeClass>>() { });
this worked for me;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
#Getter
#Setter
#NoArgsConstructor
public class Entity<T> {
private T data;
#JsonSerialize(converter = ClassToStringConverter.class)
#JsonDeserialize(converter = StringToClassConverter.class)
private Class<T> dataClass;
}
and
import com.fasterxml.jackson.databind.util.StdConverter;
public class ClassToStringConverter extends StdConverter<Class<?>, String> {
public String convert(Class<?> aClass) {
// class java.lang.Integer
return aClass.toString().split("\\s")[1];
}
}
and
import com.fasterxml.jackson.databind.util.StdConverter;
public class StringToClassConverter extends StdConverter<String, Class<?>> {
public Class<?> convert(String s) {
try {
return Class.forName(s);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
Main;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
Entity<Integer> data = new Entity<Integer>();
data.setData(5);
data.setDataClass(Integer.class);
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(data);
Entity<Integer> jsonData = mapper.readValue(json, new TypeReference<Entity<Integer>>() {});
System.out.println(jsonData.getData());
System.out.println(jsonData.getDataClass().getCanonicalName());
}
}
But, maybe it will be better, to not save the class type, but use method to get type from data?
public Class<T> getType() {
return (Class<T>) data.getClass();
}
public class Response<T> {
private T data;
// other fields & methods
public Class getType() {
return Optional.ofNullable(data).map(Object::getClass).orElse(Void.class);
}
public Optional<Class> getSafeType() {
return Optional.ofNullable(data).map(Object::getClass);
}
}
Super simple, no need to tinker with Jackson, NPE safe...
I have created a Jackson Custom Deserializer to deserialize a JSON string :
public class TestMapper extends StdDeserializer<Test> {
public TestMapper() {
this(null);
}
public TestMapper(Class<?> vc) {
super(vc);
}
#Override
public Test deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
I want to pass a "String argument" to the deserialize method that I want to use during deserialization. Is there a way to do that?
I'm calling the deserializer as follows in my code:
new ObjectMapper().readValue(json, Test.class)
and the Test Class is :
#JsonDeserialize(using = TestMapper.class)
public class Test {
You need to create constructor which takes your extra argument which will be used during deserialisation:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.io.IOException;
public class JsonApp {
public static void main(String[] args) throws Exception {
SimpleModule customModule = new SimpleModule();
customModule.addDeserializer(Test.class, new TestMapper("Extra value!!!"));
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(customModule);
Test test = new Test();
test.setValue("Value");
String json = mapper.writeValueAsString(test);
System.out.println(json);
System.out.println(mapper.readValue(json, Test.class));
}
}
class TestMapper extends StdDeserializer<Test> {
private String extraConfig;
public TestMapper() {
this(null);
}
public TestMapper(String extraConfig) {
super(Test.class);
this.extraConfig = extraConfig;
}
#Override
public Test deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
Test test = new Test();
test.setValue(extraConfig);
return test;
}
}
class Test {
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
#Override
public String toString() {
return "Test{" +
"value='" + value + '\'' +
'}';
}
}
Above code prints:
{"value":"Value"}
Test{value='Extra value!!!'}
You should always provide to super constructor your POJO class, for example, Test.class. If you need more complex initialisation, take a look on ContextualDeserializer.
Also, take a look:
How to inject dependency into Jackson Custom deserializer
Jackson - deserialize inner list of objects to list of one higher level
I have an external service which I use to query some data. The data will be in one of two formats (first of which is kind of "legacy", but needs to be supported):
{
"foo": "John Smith"
}
or
{
"foo": {
"name": "John Smith",
"bar": "baz"
}
}
which I want to map to the following POJO:
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Outer {
private Foo foo;
#Data
#AllArgsConstructor
#NoArgsConstructor
public static class Foo {
String name;
String bar;
}
}
Data in the second format (foo is an object) should be deserialized just like any other POJO, but given data in the first format (foo is string), to turn it into an instance of Foo, I want to call new Foo(<foo>, null). To do this, I have created a custom deserializer (#JsonComponent means that this deserializer will be registered with a kinda-global ObjectMapper by spring via Jackson Module interface):
#JsonComponent
public class FooDeserializer extends JsonDeserializer<Outer.Foo> {
#Override
public Outer.Foo deserialize(JsonParser parser, DeserializationContext context)
throws IOException {
JsonNode node = parser.getCodec().readTree(parser);
if (node.isTextual()) {
return new Foo(node.asText(), null);
}
return <delegate to next applicable deserializer>;
}
}
I'm having trouble figuring out how to do the "delegate to next applicable deserializer" part, as every solution I've tried (for example parser.getCodec().treeToValue(node, Outer.Foo.class)) ends up using the same custom deserializer again, causing infinite recursion. Is this even possible?
Credit to schummar answer :How do I call the default deserializer from a custom deserializer in Jackson. Following the above answer,
1. #JsonComponent annotation should be removed from the custom serializer as we need to construct the custom serializer using the default serializer, and this is not supported by #JsonComponent.
2. Register a SimpleModule to the ObjectMapper with a BeanDeserializerModifier and modify the serializer with our custom serializer constructed with the default serializer.
3. In the serialize method of the custom serializer, handle the special case, and delegate the serialization to the default serializer for normal case.
The following code demonstrates how to implement above points.
Main class
import java.io.IOException;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.module.SimpleModule;
public class DelegateDeserializer {
public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException {
ObjectMapper mapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc,
JsonDeserializer<?> deserializer) {
if (Outer.Foo.class.isAssignableFrom(beanDesc.getBeanClass())) {
return new FooDeserializer(deserializer, beanDesc.getBeanClass());
}
return deserializer;
}
});
mapper.registerModule(simpleModule);
Outer outer1 = mapper.readValue(getType1Json(), Outer.class);
Outer outer2 = mapper.readValue(getType2Json(), Outer.class);
System.out.println("deserialize json with object structure:");
System.out.println(outer1.getFoo().getName());
System.out.println(outer1.getFoo().getBar());
System.out.println("deserialize json with string field only:");
System.out.println(outer2.getFoo().getName());
System.out.println(outer2.getFoo().getBar());
}
private static String getType1Json() {
return " { "
+ " \"foo\": { "
+ " \"name\": \"John Smith\", "
+ " \"bar\": \"baz\" "
+ " } "
+ "} ";
}
private static String getType2Json() {
return " { "
+ " \"foo\": \"John Smith\" "
+ "} ";
}
}
FooDeserializer class
import java.io.IOException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.deser.ResolvableDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import jackson.Outer.Foo;
public class FooDeserializer extends StdDeserializer<Outer.Foo> implements ResolvableDeserializer {
private static final long serialVersionUID = 1L;
private final JsonDeserializer<?> defaultDeserializer;
public FooDeserializer(JsonDeserializer<?> defaultDeserializer, Class<?> clazz) {
super(clazz);
this.defaultDeserializer = defaultDeserializer;
}
#Override
public Outer.Foo deserialize(JsonParser parser, DeserializationContext context) throws IOException {
if (parser.getCurrentToken() == JsonToken.VALUE_STRING) {
JsonNode node = parser.getCodec().readTree(parser);
if (node.isTextual()) {
return new Foo(node.asText(), null);
}
}
return (Foo) defaultDeserializer.deserialize(parser, context);
}
#Override
public void resolve(DeserializationContext ctxt) throws JsonMappingException {
((ResolvableDeserializer) defaultDeserializer).resolve(ctxt);
}
}
Outer class
public class Outer {
private Foo foo;
public Foo getFoo() {
return foo;
}
public void setFoo(Foo foo) {
this.foo = foo;
}
public static class Foo {
private String bar;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getBar() {
return bar;
}
public void setBar(String bar) {
this.bar = bar;
}
public Foo() {
}
public Foo(String name, String bar) {
this.name = name;
this.bar = bar;
}
}
}
I am baffled by how registering a custom KeyDeserializer works.
Here is my code:
Matchday.java
package com.example;
import java.io.Serializable;
import java.util.Objects;
public class Matchday implements Serializable, Comparable<Matchday> {
private static final long serialVersionUID = -8823049187525703664L;
private final int matchdayNumber;
public Matchday(final int matchdayNumber) {
this.matchdayNumber = matchdayNumber;
}
public int getMatchdayNumber() {
return matchdayNumber;
}
#Override
public int compareTo(Matchday o) {
return Integer.compare(matchdayNumber, o.getMatchdayNumber());
}
#Override
public final int hashCode() {
return Objects.hash(matchdayNumber);
}
#Override
public final boolean equals(final Object obj) {
return obj instanceof Matchday && Integer.valueOf(matchdayNumber).equals(((Matchday) obj).matchdayNumber);
}
#Override
public String toString() {
return Integer.toString(matchdayNumber);
}
}
TeamPlayer.java
package com.example;
import java.io.Serializable;
import org.apache.commons.lang3.builder.ToStringBuilder;
public class TeamPlayer implements Serializable {
private static final long serialVersionUID = -6057852081020631549L;
private int id;
private String name;
private String surname;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
}
#Override
public String toString() {
return new ToStringBuilder(this).append("id", id).append("name", name).append("surname", surname).build()
.toString();
}
}
Now if I define a custom map key deserializer for my class Matchday.java, it works like a charm if I do it like this.
KeyDeserializerTest.java
package com.example;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.SortedMap;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.KeyDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
public class KeyDeserializerTest {
public static void main(String[] args) throws IOException {
final ObjectMapper objectMapper = new ObjectMapper();
final SimpleModule mySimpleModule = new SimpleModule("dummy", new Version(0, 0, 0, "dummy", "dummy", "dummy"));
mySimpleModule.addKeyDeserializer(Matchday.class, new KeyDeserializer() {
#Override
public Object deserializeKey(String arg0, DeserializationContext arg1)
throws IOException, JsonProcessingException {
return new Matchday(Integer.valueOf(arg0));
}
});
objectMapper.registerModule(mySimpleModule);
final InputStream inputStream = new ByteArrayInputStream(
"{\"1\":[{\"id\": 1, \"name\": \"Arkadiusz\", \"surname\": \"Malarz\"}]}".getBytes());
SortedMap<Matchday, List<TeamPlayer>> map = objectMapper.readValue(inputStream,
new TypeReference<SortedMap<Matchday, List<TeamPlayer>>>() {
});
System.out.println(map);
}
}
It prints
{1=[com.example.TeamPlayer#3a8624[id=1,name=Arkadiusz,surname=Malarz]]}
But if I define both the object mapper and my deserializer instances as static attributes then I get the following exception!
KeyDeserializerStaticTest.java
package com.example;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.SortedMap;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.KeyDeserializer;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
public class KeyDeserializerStaticTest {
public static final ObjectMapper OBJECT_MAPPER = createObjectMapper();
private static final KeyDeserializer MATCHDAY_KEY_DESERIALIZER = new KeyDeserializer() {
#Override
public Object deserializeKey(String key, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
return new Matchday(Integer.valueOf(key));
}
};
private static ObjectMapper createObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(createSimpleModule());
return objectMapper;
}
private static Module createSimpleModule() {
SimpleModule simpleModule = new SimpleModule("dummy", new Version(0, 0, 0, "dummy", "dummy", "dummy"));
simpleModule.addKeyDeserializer(Matchday.class, MATCHDAY_KEY_DESERIALIZER);
return simpleModule;
}
public static void main(String[] args) throws IOException {
final InputStream inputStream = new ByteArrayInputStream(
"{\"1\":[{\"id\": 1, \"name\": \"Arkadiusz\", \"surname\": \"Malarz\"}]}".getBytes());
SortedMap<Matchday, List<TeamPlayer>> map = OBJECT_MAPPER.readValue(inputStream,
new TypeReference<SortedMap<Matchday, List<TeamPlayer>>>() {
});
System.out.println(map);
}
}
Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Can not find a (Map) Key deserializer for type [simple type, class com.example.Matchday]
at [Source: java.io.ByteArrayInputStream#bbc1e0; line: 1, column: 1]
at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:270)
at com.fasterxml.jackson.databind.DeserializationContext.reportMappingException(DeserializationContext.java:1234)
at com.fasterxml.jackson.databind.deser.DeserializerCache._handleUnknownKeyDeserializer(DeserializerCache.java:585)
at com.fasterxml.jackson.databind.deser.DeserializerCache.findKeyDeserializer(DeserializerCache.java:168)
at com.fasterxml.jackson.databind.DeserializationContext.findKeyDeserializer(DeserializationContext.java:499)
at com.fasterxml.jackson.databind.deser.std.MapDeserializer.createContextual(MapDeserializer.java:247)
at com.fasterxml.jackson.databind.DeserializationContext.handleSecondaryContextualization(DeserializationContext.java:681)
at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:481)
at com.fasterxml.jackson.databind.ObjectMapper._findRootDeserializer(ObjectMapper.java:3899)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3794)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2915)
at com.example.KeyDeserializerStaticTest.main(KeyDeserializerStaticTest.java:43)
What is wrong here? Semantically there is no difference between the above presented main methods. Is this a feature that is somewhere documented or is it simply a bug in Jackson?
The root problem here was the order of initialization of static variables.
It is
public static final ObjectMapper OBJECT_MAPPER = createObjectMapper();
private static final KeyDeserializer MATCHDAY_KEY_DESERIALIZER = new KeyDeserializer() {
#Override
public Object deserializeKey(String key, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
return new Matchday(Integer.valueOf(key));
}
};
while it should be
private static final KeyDeserializer MATCHDAY_KEY_DESERIALIZER = new KeyDeserializer() {
#Override
public Object deserializeKey(String key, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
return new Matchday(Integer.valueOf(key));
}
};
public static final ObjectMapper OBJECT_MAPPER = createObjectMapper();
This was hard to spot because the method addKeyDeserializer(Class<?>, KeyDeserializer) of the class SimpleModule silently adds null references to an internal key deserializers' map. In my opinion it should throw a NullPointerException upon trying adding a key deserializer reference that is null.
The Jackson code for it looks like this.
First addKeKeyDeserializer(Class<?>, KeyDeserializer)
public SimpleModule addKeyDeserializer(Class<?> type, KeyDeserializer deser)
{
if (_keyDeserializers == null) {
_keyDeserializers = new SimpleKeyDeserializers();
}
_keyDeserializers.addDeserializer(type, deser);
return this;
}
there is no check here whether deser is null.
Then it delegates to addDeserializer(Class, KeyDeserializer) of class SimpleKeyDeserializers.
public SimpleKeyDeserializers addDeserializer(Class<?> forClass, KeyDeserializer deser)
{
if (_classMappings == null) {
_classMappings = new HashMap<ClassKey,KeyDeserializer>();
}
_classMappings.put(new ClassKey(forClass), deser);
return this;
}
Here is the null reference also ignored and silently put into _classMappings map.
Here is the issue I posted on GitHub together with the discussion.