Custom Json Deserializer in Jackson for Hashmap only - java

I'm writing json serialization (using Jackson) for a hierarchy of Java classes, i.e. the classes are composed of other classes. Since, I'm not serializing all properties, I've used JsonViews and have annotated only those properties that I want to serialize. The class at the top of this hierarchy contains a Map which also needs to be serialized/deserialized. Is it possible to write a serializer/deserializer only for the Map ? I want the default serializer to take care of serializing the rest of the objects
Why this requirement ? If I define a serializer for the topmost class, then I would need to do the serialization for all the objects. The JsonGenerator object seems to ignore the JsonView annotations and serializes all properties.

Sure its possible. You define your custom Serializer with the Map class generic type, then bind it using Jackson module subsystem.
Here is an example: (it produces silly custom serialization, but the principal is valid)
public class Test
{
// the "topmost" class
public static class DTO {
public String name = "name";
public boolean b = false;
public int i = 100;
#JsonView(MyView.class)
public Map<String, String> map; {
map = new HashMap<>();
map.put("key1", "value1");
map.put("key2", "value2");
map.put("key3", "value3");
}
}
// just to prove it works with views...
public static class MyView {}
// custom serializer for Map
public static class MapSerializer extends JsonSerializer<Map> {
#Override
public void serialize(Map map, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
// your custom serialization goes here ....
gen.writeStartObject();
gen.writeFieldName("map-keys");
gen.writeStartArray();
gen.writeString(map.keySet().toString());
gen.writeEndArray();
gen.writeFieldName("map-valuess");
gen.writeStartArray();
gen.writeString(map.values().toString());
gen.writeEndArray();
gen.writeEndObject();
}
}
public static void main(String[] args) {
SimpleModule module = new SimpleModule();
module.addSerializer(Map.class, new MapSerializer());
ObjectMapper mapper = new ObjectMapper();
mapper.disable(MapperFeature.DEFAULT_VIEW_INCLUSION);
mapper.registerModule(module);
try {
mapper.writerWithView(MyView.class).writeValue(System.out, new DTO());
} catch (Exception e) {
e.printStackTrace();
}
}
}

Related

How do you restrict custom ObjectMapper serializer to specific generic type

I would like to use a custom serializer for Maps, but only if they are <String, String> maps. I attempted to register a StdSerializer<Map,String, String>> following the advice in this post like so:
ObjectMapper om = new ObjectMapper();
SimpleModule maskModule = new SimpleModule();
JavaType mapType =
om.getTypeFactory().constructMapLikeType(Map.class, String.class, String.class);
maskModule.addSerializer(new MapSerializer(mapType));
om.registerModule(maskModule);
My Serializer looks as follows:
public static class MapSerializer extends StdSerializer<Map<String, String>> {
MapSerializer(JavaType type) {
super(type);
}
#Override
public void serialize(
Map<String, String> value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
gen.writeStartObject();
for (Map.Entry<String, String> entry : value.entrySet()) {
gen.writeStringField(entry.getKey(), doOtherStuff(entry.getValue()));
}
gen.writeEndObject();
}
}
This works fine on Map<String, String> objects, but unfortunately it also gets called for other maps as well. How does one restrict a custom serializer to specific generic types?
Thanks to the comment by #shmosel, I've identified that the following solution appears to allow us to intercept the serializer assignment by using a SerializerModifier.
public static class MapSerializerModifier extends BeanSerializerModifier {
#Override
public JsonSerializer<?> modifyMapSerializer(SerializationConfig config, MapType valueType, BeanDescription beanDesc, JsonSerializer serializer) {
if(valueType.getKeyType().getRawClass().equals(String.class) &&
valueType.getContentType().getRawClass().equals(String.class)) {
return new MapSerializer();
}
return serializer;
}
}
Then instead of registering the MapSerializer directly in the module, you register the modifier instead:
SimpleModule maskModule = new SimpleModule();
maskModule.setSerializerModifier(new MapSerializerModifier());
om.registerModule(maskModule);
Now the modifier gets checked for each Map and either returns the custom serializer or the default serializer depending on the object's type properties.

Serialize objects to a map with the object class as the key?

I'm writing a application using Spring boot and jackson for JSON parsing. I need to handle another service which produces JSON like this:
{
"task-id": 5081,
"task-created-on": {
"java.util.Date": 1631022026000
}
}
Notably, certain fields like the date field here are serialized into a map with a single key-value pair, where the key is a java classname and the value is the actual value of the field.
I've been going through the jackson documentation and haven't found anything about this format. Is there a way to configure jackson to produce and parse fields in this format?
At a minimum, I need to handle dates formatted this way. But I believe the service also uses this format for other objects, where the map key will be the name of some arbitrary java class and the value will be a map of its own. So I'd be interested in a solution that handles more than just dates if possible.
It can be easily done with custom serializer in Jackson by following steps.
First, create objects for serialization as follows:
class MyDateObject {
private Date date;
//general getter/setter
}
class Task {
#JsonProperty("task-id")
private int taskId;
#JsonProperty("task-created-on")
private MyDateObject taskCreatedOn;
//general getters/setters
}
Second, define your custom serializer: (Please note that I used myDateObject.getDate().getClass().getName() to get the class name of date field.)
class DateSerializer extends StdSerializer<MyDateObject> {
public DateSerializer() {
this(null);
}
protected DateSerializer(Class<MyDateObject> t) {
super(t);
}
#Override
public void serialize(MyDateObject myDateObject, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartObject();
jsonGenerator.writeNumberField(myDateObject.getDate().getClass().getName(), myDateObject.getDate().getTime());
jsonGenerator.writeEndObject();
}
}
Finally, register the serializer with ObjectMapper for the MyDateObject class and perform the serialization:
MyDateObject myDateObject = new MyDateObject();
myDateObject.setDate(new Date());
Task task = new Task();
task.setTaskId(5081);
task.setTaskCreatedOn(myDateObject);
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(MyDateObject.class, new DateSerializer());
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(simpleModule);
System.out.println(objectMapper.writeValueAsString(task));
The expected output is:
{"task-id":5081,"task-created-on":{"java.util.Date":1633402076254}}
Please refer to Jackson – Custom Serializer for more information.
It is possible to solve the issue with the use of a custom JsonSerializer and applying the JsonSerialize over the fields in the pojo you are interested like below :
public class Task {
#JsonProperty("task-id")
private int taskId;
#JsonProperty("task-created-on")
#JsonSerialize(using = ObjectSerializer.class)
Date taskCreatedOn;
}
The custom serializer will use the JsonGenerator.html#writeObjectField to serialize a generic object (Date or other java class) as propertyname : {"classname" : value} :
public class ObjectSerializer extends JsonSerializer<Object> {
#Override
public void serialize(Object t, JsonGenerator jg, SerializerProvider sp) throws IOException {
jg.writeStartObject();
jg.writeObjectField(t.getClass().getName(), t);
jg.writeEndObject();
}
}

Jackson, how can I use default deserializer in custom one [duplicate]

I have a problem in my custom deserializer in Jackson. I want to access the default serializer to populate the object I am deserializing into. After the population I will do some custom things but first I want to deserialize the object with the default Jackson behavior.
This is the code that I have at the moment.
public class UserEventDeserializer extends StdDeserializer<User> {
private static final long serialVersionUID = 7923585097068641765L;
public UserEventDeserializer() {
super(User.class);
}
#Override
#Transactional
public User deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
ObjectCodec oc = jp.getCodec();
JsonNode node = oc.readTree(jp);
User deserializedUser = null;
deserializedUser = super.deserialize(jp, ctxt, new User());
// The previous line generates an exception java.lang.UnsupportedOperationException
// Because there is no implementation of the deserializer.
// I want a way to access the default spring deserializer for my User class.
// How can I do that?
//Special logic
return deserializedUser;
}
}
What I need is a way to initialize the default deserializer so that I can pre-populate my POJO before I start my special logic.
When calling deserialize from within the custom deserializer It seems the method is called from the current context no matter how I construct the serializer class. Because of the annotation in my POJO. This causes a Stack Overflow exception for obvious reasons.
I have tried initializing a BeanDeserializer but the process is extremely complex and I haven't managed to find the right way to do it. I have also tried overloading the AnnotationIntrospector to no avail, thinking that it might help me ignore the annotation in the DeserializerContext. Finally it seams I might have had some success using JsonDeserializerBuilders although this required me to do some magic stuff to get hold of the application context from Spring. I would appreciate any thing that could lead me to a cleaner solution for example how Can I construct a deserialization context without reading the JsonDeserializer annotation.
As StaxMan already suggested you can do this by writing a BeanDeserializerModifier and registering it via SimpleModule. The following example should work:
public class UserEventDeserializer extends StdDeserializer<User> implements ResolvableDeserializer
{
private static final long serialVersionUID = 7923585097068641765L;
private final JsonDeserializer<?> defaultDeserializer;
public UserEventDeserializer(JsonDeserializer<?> defaultDeserializer)
{
super(User.class);
this.defaultDeserializer = defaultDeserializer;
}
#Override public User deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException
{
User deserializedUser = (User) defaultDeserializer.deserialize(jp, ctxt);
// Special logic
return deserializedUser;
}
// for some reason you have to implement ResolvableDeserializer when modifying BeanDeserializer
// otherwise deserializing throws JsonMappingException??
#Override public void resolve(DeserializationContext ctxt) throws JsonMappingException
{
((ResolvableDeserializer) defaultDeserializer).resolve(ctxt);
}
public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException
{
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier()
{
#Override public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer)
{
if (beanDesc.getBeanClass() == User.class)
return new UserEventDeserializer(deserializer);
return deserializer;
}
});
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
User user = mapper.readValue(new File("test.json"), User.class);
}
}
The DeserializationContext has a readValue() method you may use. This should work for both the default deserializer and any custom deserializers you have.
Just be sure to call traverse() on the JsonNode level you want to read to retrieve the JsonParser to pass to readValue().
public class FooDeserializer extends StdDeserializer<FooBean> {
private static final long serialVersionUID = 1L;
public FooDeserializer() {
this(null);
}
public FooDeserializer(Class<FooBean> t) {
super(t);
}
#Override
public FooBean deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
FooBean foo = new FooBean();
foo.setBar(ctxt.readValue(node.get("bar").traverse(), BarBean.class));
return foo;
}
}
I found an answer at https://stackoverflow.com/a/51927577/14731 which is much more readable than the accepted answer.
public User deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
User user = jp.readValueAs(User.class);
// some code
return user;
}
It really doesn't get easier than this.
If it is possible for you to declare extra User class then you can implement it just using annotations
// your class
#JsonDeserialize(using = UserEventDeserializer.class)
public class User {
...
}
// extra user class
// reset deserializer attribute to default
#JsonDeserialize
public class UserPOJO extends User {
}
public class UserEventDeserializer extends StdDeserializer<User> {
...
#Override
public User deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
// specify UserPOJO.class to invoke default deserializer
User deserializedUser = jp.ReadValueAs(UserPOJO.class);
return deserializedUser;
// or if you need to walk the JSON tree
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
JsonNode node = oc.readTree(jp);
// specify UserPOJO.class to invoke default deserializer
User deserializedUser = mapper.treeToValue(node, UserPOJO.class);
return deserializedUser;
}
}
There are couple of ways to do this, but to do it right involves bit more work. Basically you can not use sub-classing, since information default deserializers need is built from class definitions.
So what you can most likely use is to construct a BeanDeserializerModifier, register that via Module interface (use SimpleModule). You need to define/override modifyDeserializer, and for the specific case where you want to add your own logic (where type matches), construct your own deserializer, pass the default deserializer you are given.
And then in deserialize() method you can just delegate call, take the result Object.
Alternatively, if you must actually create and populate the object, you can do so and call overloaded version of deserialize() that takes third argument; object to deserialize into.
Another way that might work (but not 100% sure) would be to specify Converter object (#JsonDeserialize(converter=MyConverter.class)). This is a new Jackson 2.2 feature.
In your case, Converter would not actually convert type, but simplify modify the object: but I don't know if that would let you do exactly what you want, since the default deserializer would be called first, and only then your Converter.
Along the lines of what Tomáš Záluský has suggested, in cases where using BeanDeserializerModifier is undesirable you can construct a default deserializer yourself using BeanDeserializerFactory, although there is some extra setup necessary. In context, this solution would look like so:
public User deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
ObjectCodec oc = jp.getCodec();
JsonNode node = oc.readTree(jp);
User deserializedUser = null;
DeserializationConfig config = ctxt.getConfig();
JavaType type = TypeFactory.defaultInstance().constructType(User.class);
JsonDeserializer<Object> defaultDeserializer = BeanDeserializerFactory.instance.buildBeanDeserializer(ctxt, type, config.introspect(type));
if (defaultDeserializer instanceof ResolvableDeserializer) {
((ResolvableDeserializer) defaultDeserializer).resolve(ctxt);
}
JsonParser treeParser = oc.treeAsTokens(node);
config.initialize(treeParser);
if (treeParser.getCurrentToken() == null) {
treeParser.nextToken();
}
deserializedUser = (User) defaultDeserializer.deserialize(treeParser, context);
return deserializedUser;
}
You are bound to fail if you try to create your custom deserializer from scratch.
Instead, you need to get hold of the (fully configured) default deserializer instance through a custom BeanDeserializerModifier, and then pass this instance to your custom deserializer class:
public ObjectMapper getMapperWithCustomDeserializer() {
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
BeanDescription beanDesc, JsonDeserializer<?> defaultDeserializer)
if (beanDesc.getBeanClass() == User.class) {
return new UserEventDeserializer(defaultDeserializer);
} else {
return defaultDeserializer;
}
}
});
objectMapper.registerModule(module);
return objectMapper;
}
Note: This module registration replaces the #JsonDeserialize annotation, i.e. the User class or User fields should no longer be annotated with this annotation.
The custom deserializer should then be based on a DelegatingDeserializer so that all methods delegate, unless you provide an explicit implementation:
public class UserEventDeserializer extends DelegatingDeserializer {
public UserEventDeserializer(JsonDeserializer<?> delegate) {
super(delegate);
}
#Override
protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegate) {
return new UserEventDeserializer(newDelegate);
}
#Override
public User deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException {
User result = (User) super.deserialize(p, ctxt);
// add special logic here
return result;
}
}
I was not ok with using BeanSerializerModifier since it forces to declare some behavioral changes in central ObjectMapper rather than in custom deserializer itself and in fact it is parallel solution to annotating entity class with JsonSerialize. If you feel it the similar way, you might appreciate my answer here: https://stackoverflow.com/a/43213463/653539
Using BeanDeserializerModifier works well, but if you need to use JsonDeserialize there is a way to do it with AnnotationIntrospector
like this:
ObjectMapper originalMapper = new ObjectMapper();
ObjectMapper copy = originalMapper.copy();//to keep original configuration
copy.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {
#Override
public Object findDeserializer(Annotated a) {
Object deserializer = super.findDeserializer(a);
if (deserializer == null) {
return null;
}
if (deserializer.equals(MyDeserializer.class)) {
return null;
}
return deserializer;
}
});
Now copied mapper will now ignore your custom deserializer (MyDeserializer.class) and use default implementation. You can use it inside deserialize method of your custom deserializer to avoid recursion by making copied mapper static or wire it if using Spring.
A simpler solution for me was to just add another bean of ObjectMapper and use that to deserialize the object (thanks to https://stackoverflow.com/users/1032167/varren comment) - in my case I was interested to either deserialize to its id (an int) or the whole object https://stackoverflow.com/a/46618193/986160
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.springframework.context.annotation.Bean;
import java.io.IOException;
public class IdWrapperDeserializer<T> extends StdDeserializer<T> {
private Class<T> clazz;
public IdWrapperDeserializer(Class<T> clazz) {
super(clazz);
this.clazz = clazz;
}
#Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
return mapper;
}
#Override
public T deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
String json = jp.readValueAsTree().toString();
// do your custom deserialization here using json
// and decide when to use default deserialization using local objectMapper:
T obj = objectMapper().readValue(json, clazz);
return obj;
}
}
for each entity that needs to be going through custom deserializer we need to configure it in the global ObjectMapper bean of the Spring Boot App in my case (e.g for Category):
#Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
SimpleModule testModule = new SimpleModule("MyModule")
.addDeserializer(Category.class, new IdWrapperDeserializer(Category.class))
mapper.registerModule(testModule);
return mapper;
}
Here is a short solution using default ObjectMapper
private static final ObjectMapper MAPPER = new ObjectMapper(); // use default mapper / mapper without customization
public MyObject deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
MyObject object = MAPPER.readValue(p, MyObject.class);
// do whatever you want
return object;
}
And please: There is really no need to use any String value or something else. All needed information are given by JsonParser, so use it.

Edit String field value when serializing object with Jackson

In Spring MVC project, I have controllers which return data in the form of various objects. Later these objects are serialized to JSON, and JSON returned as a response to a client.
This is achieved by registering custom MappingJackson2HttpMessageConverter with configureMessageConverters() in application config. The converter uses
jackson.databind.ObjectWriter.writeValue(jackson.core.JsonGenerator, object)
for object serialization.
Now I need to implement XSS protection. Since all data goes through the step of serialization, I want to filter here String values, removing all symbols except those in whitelist (alphanumeric and some punctuation signs).
Is there a way to provide Jackson with a filter which will edit String values during serialization?
I have an instance of MappingJackson2HttpMessageConverter. There, in method writeInternal(Object object, HttpOutputMessage outputMessage), I create an instance of my custom mapper: mapper = new KJsonMapper(); This mapper is then used there to generate JSON.
KJsonMapper.java:
public class KJsonMapper extends ObjectMapper {
public KJsonMapper() {
enableAntiXSS();
}
private void enableAntiXSS() {
SimpleModule module = new SimpleModule("Anti-XSS Serializer",
new Version(1, 0, 0, "FINAL", "klab", "klab.anti-xss-serializer"));
module.addSerializer(String.class, new KJsonAntiXssSerializer());
registerModule(module);
}
}
The mapper itself uses custom JsonSerializer, which is to provide anti-xss filtering.
KJsonAntiXssSerializer.java:
public class KJsonAntiXssSerializer extends JsonSerializer<String> {
public void serialize(String value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
if (value == null) {
return;
}
String encodedValue = antiXss(value);
jsonGenerator.writeString(encodedValue);
}
private String antiXss(String value) {
// return processed value from here
}
}

How do I call the default deserializer from a custom deserializer in Jackson

I have a problem in my custom deserializer in Jackson. I want to access the default serializer to populate the object I am deserializing into. After the population I will do some custom things but first I want to deserialize the object with the default Jackson behavior.
This is the code that I have at the moment.
public class UserEventDeserializer extends StdDeserializer<User> {
private static final long serialVersionUID = 7923585097068641765L;
public UserEventDeserializer() {
super(User.class);
}
#Override
#Transactional
public User deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
ObjectCodec oc = jp.getCodec();
JsonNode node = oc.readTree(jp);
User deserializedUser = null;
deserializedUser = super.deserialize(jp, ctxt, new User());
// The previous line generates an exception java.lang.UnsupportedOperationException
// Because there is no implementation of the deserializer.
// I want a way to access the default spring deserializer for my User class.
// How can I do that?
//Special logic
return deserializedUser;
}
}
What I need is a way to initialize the default deserializer so that I can pre-populate my POJO before I start my special logic.
When calling deserialize from within the custom deserializer It seems the method is called from the current context no matter how I construct the serializer class. Because of the annotation in my POJO. This causes a Stack Overflow exception for obvious reasons.
I have tried initializing a BeanDeserializer but the process is extremely complex and I haven't managed to find the right way to do it. I have also tried overloading the AnnotationIntrospector to no avail, thinking that it might help me ignore the annotation in the DeserializerContext. Finally it seams I might have had some success using JsonDeserializerBuilders although this required me to do some magic stuff to get hold of the application context from Spring. I would appreciate any thing that could lead me to a cleaner solution for example how Can I construct a deserialization context without reading the JsonDeserializer annotation.
As StaxMan already suggested you can do this by writing a BeanDeserializerModifier and registering it via SimpleModule. The following example should work:
public class UserEventDeserializer extends StdDeserializer<User> implements ResolvableDeserializer
{
private static final long serialVersionUID = 7923585097068641765L;
private final JsonDeserializer<?> defaultDeserializer;
public UserEventDeserializer(JsonDeserializer<?> defaultDeserializer)
{
super(User.class);
this.defaultDeserializer = defaultDeserializer;
}
#Override public User deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException
{
User deserializedUser = (User) defaultDeserializer.deserialize(jp, ctxt);
// Special logic
return deserializedUser;
}
// for some reason you have to implement ResolvableDeserializer when modifying BeanDeserializer
// otherwise deserializing throws JsonMappingException??
#Override public void resolve(DeserializationContext ctxt) throws JsonMappingException
{
((ResolvableDeserializer) defaultDeserializer).resolve(ctxt);
}
public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException
{
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier()
{
#Override public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer)
{
if (beanDesc.getBeanClass() == User.class)
return new UserEventDeserializer(deserializer);
return deserializer;
}
});
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
User user = mapper.readValue(new File("test.json"), User.class);
}
}
The DeserializationContext has a readValue() method you may use. This should work for both the default deserializer and any custom deserializers you have.
Just be sure to call traverse() on the JsonNode level you want to read to retrieve the JsonParser to pass to readValue().
public class FooDeserializer extends StdDeserializer<FooBean> {
private static final long serialVersionUID = 1L;
public FooDeserializer() {
this(null);
}
public FooDeserializer(Class<FooBean> t) {
super(t);
}
#Override
public FooBean deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
FooBean foo = new FooBean();
foo.setBar(ctxt.readValue(node.get("bar").traverse(), BarBean.class));
return foo;
}
}
I found an answer at https://stackoverflow.com/a/51927577/14731 which is much more readable than the accepted answer.
public User deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
User user = jp.readValueAs(User.class);
// some code
return user;
}
It really doesn't get easier than this.
If it is possible for you to declare extra User class then you can implement it just using annotations
// your class
#JsonDeserialize(using = UserEventDeserializer.class)
public class User {
...
}
// extra user class
// reset deserializer attribute to default
#JsonDeserialize
public class UserPOJO extends User {
}
public class UserEventDeserializer extends StdDeserializer<User> {
...
#Override
public User deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
// specify UserPOJO.class to invoke default deserializer
User deserializedUser = jp.ReadValueAs(UserPOJO.class);
return deserializedUser;
// or if you need to walk the JSON tree
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
JsonNode node = oc.readTree(jp);
// specify UserPOJO.class to invoke default deserializer
User deserializedUser = mapper.treeToValue(node, UserPOJO.class);
return deserializedUser;
}
}
There are couple of ways to do this, but to do it right involves bit more work. Basically you can not use sub-classing, since information default deserializers need is built from class definitions.
So what you can most likely use is to construct a BeanDeserializerModifier, register that via Module interface (use SimpleModule). You need to define/override modifyDeserializer, and for the specific case where you want to add your own logic (where type matches), construct your own deserializer, pass the default deserializer you are given.
And then in deserialize() method you can just delegate call, take the result Object.
Alternatively, if you must actually create and populate the object, you can do so and call overloaded version of deserialize() that takes third argument; object to deserialize into.
Another way that might work (but not 100% sure) would be to specify Converter object (#JsonDeserialize(converter=MyConverter.class)). This is a new Jackson 2.2 feature.
In your case, Converter would not actually convert type, but simplify modify the object: but I don't know if that would let you do exactly what you want, since the default deserializer would be called first, and only then your Converter.
Along the lines of what Tomáš Záluský has suggested, in cases where using BeanDeserializerModifier is undesirable you can construct a default deserializer yourself using BeanDeserializerFactory, although there is some extra setup necessary. In context, this solution would look like so:
public User deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
ObjectCodec oc = jp.getCodec();
JsonNode node = oc.readTree(jp);
User deserializedUser = null;
DeserializationConfig config = ctxt.getConfig();
JavaType type = TypeFactory.defaultInstance().constructType(User.class);
JsonDeserializer<Object> defaultDeserializer = BeanDeserializerFactory.instance.buildBeanDeserializer(ctxt, type, config.introspect(type));
if (defaultDeserializer instanceof ResolvableDeserializer) {
((ResolvableDeserializer) defaultDeserializer).resolve(ctxt);
}
JsonParser treeParser = oc.treeAsTokens(node);
config.initialize(treeParser);
if (treeParser.getCurrentToken() == null) {
treeParser.nextToken();
}
deserializedUser = (User) defaultDeserializer.deserialize(treeParser, context);
return deserializedUser;
}
You are bound to fail if you try to create your custom deserializer from scratch.
Instead, you need to get hold of the (fully configured) default deserializer instance through a custom BeanDeserializerModifier, and then pass this instance to your custom deserializer class:
public ObjectMapper getMapperWithCustomDeserializer() {
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
BeanDescription beanDesc, JsonDeserializer<?> defaultDeserializer)
if (beanDesc.getBeanClass() == User.class) {
return new UserEventDeserializer(defaultDeserializer);
} else {
return defaultDeserializer;
}
}
});
objectMapper.registerModule(module);
return objectMapper;
}
Note: This module registration replaces the #JsonDeserialize annotation, i.e. the User class or User fields should no longer be annotated with this annotation.
The custom deserializer should then be based on a DelegatingDeserializer so that all methods delegate, unless you provide an explicit implementation:
public class UserEventDeserializer extends DelegatingDeserializer {
public UserEventDeserializer(JsonDeserializer<?> delegate) {
super(delegate);
}
#Override
protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegate) {
return new UserEventDeserializer(newDelegate);
}
#Override
public User deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException {
User result = (User) super.deserialize(p, ctxt);
// add special logic here
return result;
}
}
I was not ok with using BeanSerializerModifier since it forces to declare some behavioral changes in central ObjectMapper rather than in custom deserializer itself and in fact it is parallel solution to annotating entity class with JsonSerialize. If you feel it the similar way, you might appreciate my answer here: https://stackoverflow.com/a/43213463/653539
Using BeanDeserializerModifier works well, but if you need to use JsonDeserialize there is a way to do it with AnnotationIntrospector
like this:
ObjectMapper originalMapper = new ObjectMapper();
ObjectMapper copy = originalMapper.copy();//to keep original configuration
copy.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {
#Override
public Object findDeserializer(Annotated a) {
Object deserializer = super.findDeserializer(a);
if (deserializer == null) {
return null;
}
if (deserializer.equals(MyDeserializer.class)) {
return null;
}
return deserializer;
}
});
Now copied mapper will now ignore your custom deserializer (MyDeserializer.class) and use default implementation. You can use it inside deserialize method of your custom deserializer to avoid recursion by making copied mapper static or wire it if using Spring.
A simpler solution for me was to just add another bean of ObjectMapper and use that to deserialize the object (thanks to https://stackoverflow.com/users/1032167/varren comment) - in my case I was interested to either deserialize to its id (an int) or the whole object https://stackoverflow.com/a/46618193/986160
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.springframework.context.annotation.Bean;
import java.io.IOException;
public class IdWrapperDeserializer<T> extends StdDeserializer<T> {
private Class<T> clazz;
public IdWrapperDeserializer(Class<T> clazz) {
super(clazz);
this.clazz = clazz;
}
#Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
return mapper;
}
#Override
public T deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
String json = jp.readValueAsTree().toString();
// do your custom deserialization here using json
// and decide when to use default deserialization using local objectMapper:
T obj = objectMapper().readValue(json, clazz);
return obj;
}
}
for each entity that needs to be going through custom deserializer we need to configure it in the global ObjectMapper bean of the Spring Boot App in my case (e.g for Category):
#Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
SimpleModule testModule = new SimpleModule("MyModule")
.addDeserializer(Category.class, new IdWrapperDeserializer(Category.class))
mapper.registerModule(testModule);
return mapper;
}
Here is a short solution using default ObjectMapper
private static final ObjectMapper MAPPER = new ObjectMapper(); // use default mapper / mapper without customization
public MyObject deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
MyObject object = MAPPER.readValue(p, MyObject.class);
// do whatever you want
return object;
}
And please: There is really no need to use any String value or something else. All needed information are given by JsonParser, so use it.

Categories

Resources