Changing the default serialization behavior in Jackson - java

I am using the Jackson library to write custom serializers, and register them inside of a custom ObjectMapper. However, I also want to change the default serialization to simply output the string representation of the object when a more specific custom serialization has not been written.
For example, say I have written custom serializers for classes "Map" and "Entry", in addition to the default serializer. Then the serialization module within my custom ObjectMapper might look like this:
SimpleModule module = new SimpleModule("module", new Version(0, 1, 0, "alpha", null, null));
module.addSerializer(Entry.class, new EntryJsonSerializer());
module.addSerializer(Map.class, new MapJsonSerializer());
module.addSerializer(Object.class, new DefaultJsonSerializer());
this.registerModule(module);
However, I'm finding that the module will use DefaultJsonSerializer to serialize Map and Entry objects (as they are also Object objects).
How can I change the default serialization behavior, while ensuring Entry and Map objects are serialized as intended?

The problem is probably that the actual type of values (say, String) is used for locating serializers.
One solution would be to register serializers for value types, if you know them.
Alternatively, you could force use of static typing; this would make serializer lookup use declared (static) type, not actual runtime type.
This can be done with:
objectMapper.enable(MapperFeature.USE_STATIC_TYPING);

I've gotten around the problem by writing a single serializer and using a series of if statements to implement prioritization:
public final class UnifiedJsonSerializer extends JsonSerializer<Object> {
#Override
public void serialize(Object object, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException {
if (object instanceof Entry) {
// Entry serialization code
} else if (object instanceof Map) {
// Map serialization code
} else {
// default serialization code
}
}

Related

If a java pojo is declared as an object, how do you control jackson serialization based on the run time type?

For example, if I declare a Map<String,Object> with a mixed type value set and I try to serialize with a custom TypeResolverBuilder, I don't have access to the run time type of the value.
But if I want to write type information for some of the values and not others, how can that be accomplished? I've been reading into the DefaultSerializerProvider and it seems to ignore the run time type and just use the JavaType (which is Object.class)
We're using Jackson 2.9
If you are able to edit your POJOs where you need to control the output you should be able to extend JsonSerializer and only write the information you want in the output of the serialize method.
You could also be able to annotate your map to provide a custom serializer and interogate the object to be serialized using instanceof
#JsonSerialize(keyUsing = SpecialSerializer.class)
Map<String, Object> map;
And then in your serializer:
#Override
public void serialize(Object value,
JsonGenerator gen,
SerializerProvider serializers)
throws IOException, JsonProcessingException {
if(value instanceof MySpecialObject){
//special logic here
}
}

Avoid Serializing null fields with Jackson custom JsonSerializer and JsonGenerator

Jackson serialization to JSON has some nice features like SerializationFeature.WRITE_NULL_MAP_VALUES which allows you to avoid writing nulls into your serialized object.
However, if I use a custom JsonSerializer this setting is ignored and I'm forced to put null checks in all my calls to writeStringField. For example, I register a custom JsonSerializer:
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(CloudDocument.class, new ImportProductSerializer());
objectMapper.registerModule(simpleModule);
and then inside of ImportProductSerializer I'm stuck with putting a null check for every field I want to serialize which gets ugly and tedious:
private class ImportProductSerializer extends JsonSerializer<CloudDocument> {
#Override
public void serialize(CloudDocument value,
JsonGenerator gen, //Customize this?
SerializerProvider serializers) throws Exception {
gen.writeStartObject();
gen.writeStringField("type", "add");
//Null check here and every other field: gross!
if(StringUtils.hasText(importProduct.getExtraListingSku1())) {
gen.writeStringField("extralistingsku1", importProduct.getExtraListingSku1());
}
gen.writeEndObject();
}
}
The solution I would like to employ is having my custom JsonSerializer use a custom JsonGenerator. The serialize method takes JsonGenerator in as a parameter and I could easily define it like so:
private class CustomGenerator extends JsonGenerator {
#Override
public JsonGenerator setCodec(ObjectCodec oc) {
super.setCodec(oc);
}
#Override
public void writeStringField(String fieldName, String value) throws IOException {
if(value==null) return;
super.writeStringField(fieldName.toLowerCase(), value);
}
.... //other methods implemented
}
but I'm not sure how to configure the ObjectMapper to use my implementation of CustomGenerator. The benefit of this approach is I could customize the writeField methods to do the null checks and to make sure that the field names subscribe to the naming conventions that are expected which, in this case, involve converting camelCase to lower_underscore.
Is there a way to configure the JsonGenerator that is instantiated with the call to objectMapper.writeValue()?
The question is bit open-ended, as I do not know exactly what you have tried so far; how much of the work is done by custom serializer.
In general serializers are expected to delegate much of the handling to minimize amount of work they do.
But if you have defined a custom serializer to handle POJOs with included fields (instead of a custom serializer for something serialized as a JSON String, or other scalar values), yes, you do need to handle your own checks. This is partly because there are many ways to potentially change inclusion; not just SerializationFeature, but also #JsonFormat annotation for properties; there is no way to handle this transparently when delegating control to custom serializers.
So, yes, if you end up using writeStringField(), you do need to check for default inclusion settings and omit write appropriately.

Implicit default values when deserializing JSON using Jackson

When deserializing a variety of JSON messages, I want to provide a default value for attributes of a certain type. It is generally suggested to simply specify the value in the Class, but this is error-prone if you have to do this across many Classes. You might forget one and end up with null instead of a default value. My intention is to set every property that is an Optional<T> to Optional.absent. Since null is exactly what Optional is trying to eliminate, using them with Jackson has proven to be frustrating.
Most features of Jackson that allow you to customize the deserialization process focus on the JSON that is the input, not around the process of instantiating the Object that you are deserializing into. The closest I seem to be getting to a general solution is by building my own ValueInstantiator, but there are two remaining issues I have:
how do I make it only instantiate Optional as absent but not interfere with the rest of the instantiation process?
how do I wire the end result into my ObjectMapper?
UPDATE: I want to clarify that I am looking for a solution that does not involve modifying each Class that contains Optional's. I'm opposed to violating the DRY principle. Me or my colleagues should not have to think about having to do something extra every time we add Optional's to a new or existing Class. I want to be able to say, "make every Optional field in every Class I deserialize into, pre-filled with Absent", only once, and be done with it.
That means the following are out:
abstract parent class (need to declare)
custom Builder/Creator/JsonDeserializer (needs annotation on each applicable class)
MixIn's? I tried this, combined with reflection, but I don't know how to access the Class I'm being mixed into...
Specifically for java.lang.Optional, there is a module by the Jackson guys themselves: https://github.com/FasterXML/jackson-datatype-jdk8
Guava Optional is covered by https://github.com/FasterXML/jackson-datatype-guava
It will create a Optional.absent for null's, but not for absent JSON values :-(.
See https://github.com/FasterXML/jackson-databind/issues/618 and https://github.com/FasterXML/jackson-datatype-jdk8/issues/2.
So you're stuck with initializing your Optionals just as you should initialize collections. It is a good practice, so you should be able to enforce it.
private Optional<Xxx> xxx = Optional.absent();
private List<Yyy> yyys = Lists.newArrayList();
You can write a custom deserializer to handle the default value. Effectively you will extend the appropriate deserializer for the type of object you are deserializing, get the deserialized value, and if it's null just return the appropriate default value.
Here's a quick way to do it with Strings:
public class DefaultStringModule extends SimpleModule {
private static final String NAME = "DefaultStringModule";
private static final String DEFAULT_VALUE = "[DEFAULT]";
public DefaultStringModule() {
super(NAME, ModuleVersion.instance.version());
addDeserializer(String.class, new DefaultStringDeserializer());
}
private static class DefaultStringDeserializer extends StdScalarDeserializer<String> {
public DefaultStringDeserializer() {
super(String.class);
}
public String deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException, JsonProcessingException {
String deserialized = jsonParser.getValueAsString();
// Use a default value instead of null
return deserialized == null ? DEFAULT_VALUE : deserialized;
}
#Override
public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer typeDeserializer) throws IOException {
return deserialize(jp, ctxt);
}
}
}
To use this with an ObjectMapper you can register the module on the instance:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new DefaultStringModule());
To handle default values for fields not present in the JSON, I've typically seen this done through the use of a builder class that will construct the class using the values supplied and add any default values for the missing fields. Then, on the deserialized class (e.g. MyClass), add a #JsonDeserialize(builder = MyClass.Builder.class) annotation to indicate to Jackson to deserialize MyClass by way of the builder class.
Your value object should initialize these values as absent. That's the way to ensure that default values have no nulls. Guava module's Optional handler really should only deserialize them as "absent" (even with explicit JSON nulls), and never as nulls, with later versions.
But since Jackson only operates on JSON properties that exist (and not on things that could exist but do not), POJO still needs to have default absent assignment.

Why doesn't Jackson use serialize() Method of custom wrapper class extending LinkedHashMap

I am using Jackson 2.3.2 library in a Spring MVC project and trying to specify a custom JSON serialization on a custom wrapper Object by implementing the JSONSerializableWithType interface which includes a method called serialize() that Jackson calls, when trying to serialize an Object instance to JSON.
Strangely this serialize method is called correctly as long as my Object wrapper does not extend LinkedHashMap<...>.
If my Object wrapper extends a Class that Jackson "knows" how to serialize in a default way, the serialize() method is not called anymore.
My wrapper class looks like this:
public class ResponseRoomOccupancy
extends LinkedHashMap<...>
implements org.codehaus.jackson.map.JsonSerializableWithType {
#Override
public void serialize(JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException {
jgen.writeStartObject();
// actual serialization of the object
jgen.writeEndObject();
}
#Override
public void serializeWithType(JsonGenerator jgen,
SerializerProvider provider, TypeSerializer typeSer)
throws IOException, JsonProcessingException {
serialize(jgen, provider);
}
}
The controller:
#RequestMapping(value = "/occupancy")
public #ResponseBody ResponseRoomOccupancy
getRoomOccupancy(RequestRoomOccupancy request) {
return appointmentService.getEnrichedRoomOccupancy(request);
}
Can anybody explain to me why Jackson does not use the custom-defined serialize method on a class that extends LinkedHashMap?
UPDATE:
As Sotirios Delimanolis pointed out correctly, the JSONSerializableWithType interface is part of an older version of Jackson (<1.9).
However I dont know why this kind of custom serialization works with custom defined classes (e.g. if LinkedHashMap is exchanged with some class Foo<...>).
You're using Jackson 2 which is completely incompatible with Jackson 1. JsonSerializableWithType is an interface from Jackson 1. You can't have them work together. Jackson 2 simply doesn't look for JsonSerializableWithType.
Instead, annotate your LinkedHashMap type with
#JsonSerialize(using = YourSerializer.class)
and have YourSerializer do the work.
Regarding your comment and edit, Jackson has some default serializers/deserializers for known types, like List, Set, String, Number, and Map. However, it does not know your custom types. It must build a new serializer based on what it finds from analyzing your type.

How to instantiate beans in custom way with Jackson?

What is the best and easiest way to instantiate beans in custom way (not by calling default constructor) while deserializing from JSON using Jackson library? I found that there is JsonDeserializer interface that I could implement, but I'm not too sure how to wire it all together into the ObjectMapper.
UPDATE #1: I think some more details is required for my question. By default Jackson's deserializer uses default constructor to crete beans. I'd like to be able to implement instantiation of the bean by calling external factory. So what I need is just a class of the bean that needs to be instantiated. The factory will return instance that can then be provided to Jackson for property population and so on.
Please note that I'm not concerned about creation of simple/scalar values such as strings or numbers, only the beans are in the area of my interest.
Some things that may help...
First, you can use #JsonCreator to define alternate constructor to use (all arguments must be annotated with #JsonProperty because bytecode has no names), or a static factory. It would still be part of value class, but can help support immutable objects.
Second, if you want truly custom deserialization process, you can check out https://github.com/FasterXML/jackson-docs/wiki/JacksonHowToCustomSerializers which explains how to register custom deserializers.
One thing that Jackson currently misses is support for builder-style objects; there is a feature request to add support (and plan is to add support in future once developers have time).
You put the deserializer on the Java object you want to get the json mapped into:
#JsonDeserialize(using = PropertyValueDeserializer.class)
public class PROPERTY_VALUE implements Serializable{
and then in the Deserializer you can e.g. do:
#Override
public PROPERTY_VALUE deserialize(JsonParser jsonParser,
DeserializationContext deserializationContext)
throws IOException, JsonProcessingException {
String tmp = jsonParser.getText(); // {
jsonParser.nextToken();
String key = jsonParser.getText();
jsonParser.nextToken();
String value = jsonParser.getText();
jsonParser.nextToken();
tmp = jsonParser.getText(); // }
PROPERTY_VALUE pv = new PROPERTY_VALUE(key,value);
return pv;
}
If you don't want to use annotations, you need to pass the mapper a DeserializerProvider that can provide the right deserializer for a given type.
mapper.setDeserializerProvider(myDeserializerProvider);
For the constructors - of course you can generate the target class by calling a factory within the deserializer:
String value = jsonParser.getText();
jsonParser.nextToken();
tmp = jsonParser.getText(); // }
MyObject pv = MyObjectFactory.get(key);
pv.setValue(value);
return pv;
}
But then I may have misunderstood your update

Categories