Jackson & JSONAnySetter: NullPointer Exception during Serialization/Deserialization - java

I have a problem with serialization/deserialization with Jackson 1.9.13 (and Jackson 2.5.0) and fighting this now for a few days without any success.
My goal is to use #JsonAnyGetter & #JsonAnySetter, and I want to calculate dynamically if a object should be written to the output or not. I have a JSON definition which I serialize with the ObjectMapper (and check if the Object should be included or not), and then I convert the object back to a string.
I am using a "HidableSerializer" for this, which works fine during serialization, but not when converting the object back to a string.
Without #JsonAnySetter / -getter or the "HidableSerializer", everythings works fine, but not both together.
Why is this not working? And how can I solve the problem? Better approaches are welcome!
The stack trace looks like this:
Stack Trace
org.codehaus.jackson.map.JsonMappingException: (was java.lang.NullPointerException) (through reference chain: ch.hasselba.Test["[anySetter]"])
null
at org.codehaus.jackson.map.JsonMappingException.wrapWithPath(JsonMappingException.java:218)
at org.codehaus.jackson.map.JsonMappingException.wrapWithPath(JsonMappingException.java:183)
at org.codehaus.jackson.map.ser.std.SerializerBase.wrapAndThrow(SerializerBase.java:140)
at org.codehaus.jackson.map.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:158)
at org.codehaus.jackson.map.ser.BeanSerializer.serialize(BeanSerializer.java:112)
at ch.hasselba.HidableSerializer.serialize(HidableSerializer.java:29)
at org.codehaus.jackson.map.ser.StdSerializerProvider._serializeValue(StdSerializerProvider.java:610)
at org.codehaus.jackson.map.ser.StdSerializerProvider.serializeValue(StdSerializerProvider.java:256)
at org.codehaus.jackson.map.ObjectMapper._configAndWriteValue(ObjectMapper.java:2575)
at org.codehaus.jackson.map.ObjectMapper.writeValueAsString(ObjectMapper.java:2097)
at ch.hasselba.Demo.main(Demo.java:54)
Caused by: java.lang.NullPointerException
at org.codehaus.jackson.map.ser.std.MapSerializer.serializeFields(MapSerializer.java:243)
at org.codehaus.jackson.map.ser.AnyGetterWriter.getAndSerialize(AnyGetterWriter.java:41)
at org.codehaus.jackson.map.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:154)
... 7 more
The Demo code
package ch.hasselba;
import org.codehaus.jackson.Version;
import org.codehaus.jackson.map.JsonSerializer;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.codehaus.jackson.map.introspect.BasicBeanDescription;
import org.codehaus.jackson.map.module.SimpleModule;
import org.codehaus.jackson.map.ser.BeanSerializerModifier;
public class Demo {
public static void main(String[] args) {
ObjectMapper mapper = new ObjectMapper();
// register the module
Version version = new Version(1, 0, 0, "SNAPSHOT");
mapper.registerModule(new SimpleModule("HidableModule", version) {
#Override
public void setupModule(SetupContext context) {
super.setupModule(context);
context.addBeanSerializerModifier(new BeanSerializerModifier() {
#SuppressWarnings("unchecked")
#Override
public JsonSerializer<?> modifySerializer(SerializationConfig config, BasicBeanDescription desc,
JsonSerializer<?> serializer) {
if (IHidable.class.isAssignableFrom(desc.getBeanClass())) {
return new HidableSerializer<Object>((JsonSerializer<Object>) serializer);
}
return serializer;
}
});
}
});
// the data
String content = "{ \"foo\": \"bar\" }";
// build the Object
Test test = null;
try {
test = mapper.readValue(content, Test.class);
} catch (Exception e) {
e.printStackTrace();
}
// and now convert it back to a String
String data = null;
try {
data = mapper.writeValueAsString(test);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println( data );
}
}
Test class
package ch.hasselba;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.codehaus.jackson.annotate.JsonAnyGetter;
import org.codehaus.jackson.annotate.JsonAnySetter;
public class Test implements IHidable {
private Map<String, Object> others = new ConcurrentHashMap<String, Object>();
#JsonAnyGetter
public Map<String, Object> getOthers() {
return this.others;
}
#JsonAnySetter
public void addOther(final String name, final Object value) {
this.others.put(name, value);
}
#Override
public boolean isHidden() {
return false;
}
}
The Hidable Serializer
package ch.hasselba;
import java.io.IOException;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.map.JsonSerializer;
import org.codehaus.jackson.map.SerializerProvider;
public class HidableSerializer<T> extends JsonSerializer<T> {
private JsonSerializer<T> defaultSerializer;
public HidableSerializer(JsonSerializer<T> serializer) {
defaultSerializer = serializer;
}
#Override
public void serialize(T value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException {
if( value instanceof IHidable ){
IHidable hidableValue = (IHidable) value;
if( hidableValue.isHidden() )
return;
}
defaultSerializer.serialize(value, jgen, provider);
}
}
IHidableInterface
package ch.hasselba;
public interface IHidable {
boolean isHidden();
}

The problem is that the defaultSerializer instance, you are using inside your HidableSerializer, is a ResolvableSerializer (BeanSerializer), but as you wrap it into JsonSerializer (HidableSerializer) in your modifySerializer() method, it's resolve() method is then never invoked and it fails to initialize properly.
If you try adding the following line to your HidableSerializer.serialize() method:
...
((ResolvableSerializer)defaultSerializer).resolve(provider);
defaultSerializer.serialize(value, jgen, provider);
...
it should do the trick.
If this works for you, a more permanent solution would be to make your HidableSerializer implement ResolvableSerializer itself and just delegate resolve() to the defaultSerializer, like this:
#Override
public void resolve(SerializerProvider serializerProvider) throws JsonMappingException {
if(defaultSerializer instanceof ResolvableSerializer) {
((ResolvableSerializer)defaultSerializer).resolve(serializerProvider);
}
}

I went through debugging process and found some code:
if (ser instanceof ResolvableSerializer) {
((ResolvableSerializer) ser).resolve(provider);
}
It actually instantiates key serializer which throws NPE in your case.
Modify your HidableSerializer and it will do the trick:
public class HidableSerializer<T> extends JsonSerializer<T> implements ResolvableSerializer {
private JsonSerializer<T> defaultSerializer;
public HidableSerializer(JsonSerializer<T> serializer) {
defaultSerializer = serializer;
}
#Override
public void serialize(T value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException {
if( value instanceof IHidable ){
IHidable hidableValue = (IHidable) value;
if( hidableValue.isHidden() )
return;
}
defaultSerializer.serialize(value, jgen, provider);
}
#Override
public void resolve(SerializerProvider provider) throws JsonMappingException {
((ResolvableSerializer)defaultSerializer).resolve(provider);
}
}

Related

FasterXml - JsonSerializer HashMap

I'm using jackson-databind version 2.12.3 to serialize the return of an object that should return like this:
{
"field1":"value1",
"field2":"value2",
"links":{
"field":{
"href":"/link"
},
"test":{
"href":"/test"
}
}
}
My classes are these:
public class HrefType {
private String href = null;
...
}
public class Link extends HashMap<String, HrefType> {
private HrefType field = null;
...
}
public class MyObject {
private String field1 = null;
private String field2 = null;
private Link links = null;
...
}
The return is myObject:
MyObject myObject = new MyObject();
myObject.setField1("value1");
myObject.setField2("value2");
Link link = new Link();
link.setField(new HrefType().href("/link"));
link.put("test",new HrefType().href("/test"));
myObject.setLinks(link);
However with the default ObjectMapper the "link.setField" is ignored and the returned json is:
{
"field1":"value1",
"field2":"value2",
"links":{
"test":{
"href":"/test"
}
}
}
I tried doing some tests with JsonSerializer but couldn't do something generic for all classes that extend HashMap (these classes are generated from BerlinGroup's PSD2 YAML, so I wouldn't want to change the generated class).
Is there a generic way to do it, or should I make a serialize class for each class that extends the HashMap?
Composition
First of all, I suggest you use composition instead of inheritance in this particular case. Your code will look like the next:
private class Link {
private final HrefType field;
private final HashMap<String, HrefType> test;
public Link(HrefType field) {
this.field = field;
}
public HrefType getField() {
return field;
}
public HashMap<String, HrefType> getTest() {
return test;
}
}
And serialization will work fine, as expected.
Serializer
But in case, if you can't change the original code, you might to write your own StdSerializer. For example:
private class LinkSerializer extends StdSerializer<Link> {
public LinkSerializer() {
super(Link.class);
}
#Override
public void serialize(Link link, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartObject();
final HrefType field = link.getField();
jsonGenerator.writeObjectField("field", field);
jsonGenerator.writeObjectField("test", new HashMap<>(link));
jsonGenerator.writeEndObject();
}
}
And declare it over your Link class:
#JsonSerialize(using = LinkSerializer.class)
private static class Link extends HashMap<String, HrefType> {
private final HrefType field;
public Link(HrefType field) {
this.field = field;
}
public HrefType getField() {
return field;
}
}
based on this answer I developed this generic method of making for all objects that extend a Map:
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Map;
import org.springframework.util.ReflectionUtils;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
public class MyClassSerializer extends JsonSerializer<Object> {
private final JsonSerializer<Object> defaultSerializer;
public MyClassSerializer(JsonSerializer<Object> defaultSerializer) {
this.defaultSerializer = (defaultSerializer);
}
#SuppressWarnings({ "unchecked", "rawtypes" })
#Override
public void serialize(Object src, JsonGenerator gen, SerializerProvider provider) throws IOException {
Field[] fields = src.getClass().getDeclaredFields();
for (Field field : fields) {
try {
boolean fieldAccessible = field.isAccessible();
field.setAccessible(true);
Object object = ReflectionUtils.getField(field, src);
if (object != null && object instanceof Map) {
Field[] fieldsMap = object.getClass().getDeclaredFields();
Map map = (Map) object;
for (Field fieldMap : fieldsMap) {
boolean fieldMapAccessible = fieldMap.isAccessible();
fieldMap.setAccessible(true);
Object fieldObject = ReflectionUtils.getField(fieldMap, object);
if (fieldObject != null) {
map.put(fieldMap.getName(), fieldObject);
}
fieldMap.setAccessible(fieldMapAccessible);
}
}
field.setAccessible(fieldAccessible);
} catch (Exception e) {
e.printStackTrace();
}
}
defaultSerializer.serialize(src, gen, provider);
}
#Override
public Class<Object> handledType() {
return Object.class;
}
}
which goes through all fields, when I find one that extends from a Map I go through all the fields of this one and add it to the Map ignoring the object's fields, so the Serializer works perfectly.
EDIT: to Deserializer properly I do this:
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Map;
import org.springframework.util.ReflectionUtils;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.deser.ResolvableDeserializer;
#SuppressWarnings("rawtypes")
public class MyClassDeserializer extends JsonDeserializer implements ResolvableDeserializer {
private JsonDeserializer defaultDeserializer;
protected MyClassDeserializer(JsonDeserializer deserializer) {
this.defaultDeserializer = deserializer;
}
#SuppressWarnings("unchecked")
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
Object obj = defaultDeserializer.deserialize(p, ctxt);
Field[] fields = obj.getClass().getDeclaredFields();
for (Field field : fields) {
try {
boolean fieldAccessible = field.isAccessible();
field.setAccessible(true);
Object object = ReflectionUtils.getField(field, obj);
if (object != null && object instanceof Map) {
Field[] fieldsMap = object.getClass().getDeclaredFields();
Map map = (Map) object;
for (Object key : map.keySet()) {
for (Field fieldMap : fieldsMap) {
if (fieldMap.getName().equals((String) key)) {
if (fieldMap.getName().equalsIgnoreCase("serialVersionUID")) {
continue;
}
boolean fieldMapAccessible = fieldMap.isAccessible();
fieldMap.setAccessible(true);
Object fieldObject = ReflectionUtils.getField(fieldMap, object);
if (fieldObject == null) {
fieldMap.set(object, map.get(key));
map.replace(key, null);
}
fieldMap.setAccessible(fieldMapAccessible);
}
}
}
Object[] keys = map.keySet().toArray();
for (int i = 0; i < keys.length; i++) {
if(map.get(keys[i])==null) {
map.remove(keys[i]);
}
}
}
field.setAccessible(fieldAccessible);
} catch (Exception e) {
e.printStackTrace();
}
}
return obj;
}
#Override
public void resolve(DeserializationContext ctxt) throws JsonMappingException {
((ResolvableDeserializer) defaultDeserializer).resolve(ctxt);
}
}

Jackson won't serialize null with a custom Serializer

I have a custom bean serializer that I'd like to apply, but when I do, Jackson no longer includes null properties.
The following code reproduces the issue:
import java.io.IOException;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import lombok.Value;
public class Test {
#Value
public static class Contact {
String first;
String middle;
String last;
String email;
}
public static void main(String[] args) throws Exception {
Contact contact = new Contact("Bob", null, "Barker", null);
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new SimpleModule() {
#Override public void setupModule(SetupContext context) {
super.setupModule(context);
context.addBeanSerializerModifier(new BeanSerializerModifier() {
#Override public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription desc, JsonSerializer<?> serializer) {
// return serializer;
return new JsonSerializer<Object>() {
#Override public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
((JsonSerializer<Object>) serializer).serialize(value, gen, serializers);
}};
}
});
}
});
System.out.println(
mapper.writerWithDefaultPrettyPrinter().writeValueAsString(contact)
);
}
}
The above code does nothing other that register a 'custom' serializer (that just delegates back to the original serializer), yet it produces JSON without the null properties:
{ "first" : "Bob", "last" : "Barker" }
If you comment out the return new JsonSerializer<Object>() {... and return the passed in serializer as is return serializer;, then Jackson serializes the null properties:
{ "first" : "Bob", "middle" : null, "last" : "Barker", "email"
: null }
I have read over many seemingly related SO articles, but none have led me to a solution yet. I've tried explicitly setting the mapper to Include.ALWAYS on serialization, with no luck.
My only lead is a comment in the JavaDoc for JsonSerializer:
NOTE: various serialize methods are never (to be) called
with null values -- caller must handle null values, usually
by calling {#link SerializerProvider#findNullValueSerializer} to obtain
serializer to use.
This also means that custom serializers cannot be directly used to change
the output to produce when serializing null values.
I am using Jackson version 2.11.2.
My question is: How can I write a custom serializer and have Jackson respect its usual Include directives with regard to null property serialization?
Context Info: My actual custom serializer's job is to conditionally hide properties from serialization. I have a custom annotation, #JsonAuth that is meta-annotated with #JacksonAnnotationsInside #JsonInclude(Include.NON_EMPTY) which my custom serializer (a ContextualSerializer) looks for in an overriden isEmpty method and returns true (treat as empty) if authorization is lacking. The end result is that I have an annotation that can be applied to properties which will hide the property from serialization if the client is not authorized. Except ... usage of the custom serializer has the unintended side effect of dropping all null properties.
Update: Jackson's BeanPropertyWriter.serializeAsField(...) method will completely ignore any custom serializer assigned to the property if the value is null.
I was able to override this behavior by writing a small extension to the class, which allowed my "isAuthorized" logic to preempt the null check:
public class JsonAuthPropertyWriter extends BeanPropertyWriter {
private final Predicate<Object> authFilter;
private JsonAuthPropertyWriter(BeanPropertyWriter delegate, Predicate<Object> authFilter) {
super(delegate);
this.authFilter = authFilter;
// set null serializer or authorized null values disappear
super.assignNullSerializer(NullSerializer.instance);
}
#Override
public void serializeAsField(
Object bean,
JsonGenerator gen,
SerializerProvider prov) throws Exception {
boolean authorized = authFilter.test(bean);
if (!authorized) return;
super.serializeAsField(bean, gen, prov);
}
}
And I injected these custom BeanPropertyWriters using a BeanSerializerModifier:
private static class JsonAuthBeanSerializerModifier extends BeanSerializerModifier {
#Override
public List<BeanPropertyWriter> changeProperties(
SerializationConfig config,
BeanDescription beanDesc,
List<BeanPropertyWriter> beanProperties
) {
for (int i = 0; i < beanProperties.size(); i++) {
BeanPropertyWriter beanPropertyWriter = beanProperties.get(i);
JsonAuth jsonAuth = beanPropertyWriter.findAnnotation(JsonAuth.class);
if (jsonAuth != null) {
Predicate<Object> authPredicate = ...
beanProperties.set(i, new JsonAuthPropertyWriter(beanPropertyWriter, authPredicate));
}
}
return beanProperties;
}
}
I may be misunderstanding what you want, but this approach seems useful:
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.PropertyWriter;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.HashMap;
import java.util.Map;
public class Test2 {
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#interface JsonAuth {
}
#JsonFilter("myFilter")
public static class Contact {
#JsonAuth
String first;
#JsonAuth
String middle;
#JsonAuth
String last;
String email;
public Contact(String first, String middle, String last, String email) {
this.first = first;
this.middle = middle;
this.last = last;
this.email = email;
}
public String getFirst() {
return first;
}
public void setFirst(String first) {
this.first = first;
}
public String getMiddle() {
return middle;
}
public void setMiddle(String middle) {
this.middle = middle;
}
public String getLast() {
return last;
}
public void setLast(String last) {
this.last = last;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
public static Map<String,Boolean> fieldSerialisationCount = new HashMap<>();
public static void main(String[] args) throws Exception {
Contact contact = new Contact("Bob", null, "Barker", null);
ObjectMapper mapper = new ObjectMapper();
FilterProvider filters = new SimpleFilterProvider().addFilter("myFilter", new SimpleBeanPropertyFilter() {
#Override
protected boolean include(BeanPropertyWriter writer) {
return super.include(writer) && isAuthed(writer);
}
#Override
protected boolean include(PropertyWriter writer) {
return super.include(writer) && isAuthed(writer);
}
private boolean isAuthed(PropertyWriter writer) {
if (!writer.getMember().hasAnnotation(JsonAuth.class)) {
return true;
} else {
return fieldSerialisationCount.compute(writer.getName(), (n, b) -> b == null ? true : !b); // check auth here
}
}
});
mapper.setFilterProvider(filters);
ObjectWriter writer = mapper.writer(filters).withDefaultPrettyPrinter();
System.out.println(
writer.writeValueAsString(contact)
);
System.out.println(
writer.writeValueAsString(contact)
);
System.out.println(
writer.writeValueAsString(contact)
);
}
}
It serialises annotated fields every other time, just as an example of a filter using persistent state.
Please let me know whether this works for you.
By the way, I agree that Jackson has the problem you describe, and I don't know how to solve it, so this is a work-around rather than an answer to your original question.

Big number deserialisation throws NumberFormatException

I wrote below program to convert Parameter to JsonNode. Getting NumberFormatException when I set 3.9E38. How to set BigDecimal in JsonNode?
public class JsonCheck {
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper om = new ObjectMapper();
Parameter p = new Parameter();
p.setPrevValue(new BigDecimal("3.9E38"));
JsonNode node = om.convertValue(p,JsonNode.class);
System.out.println(node);
}
public static class DefaultValueSerializer extends JsonSerializer<BigDecimal> {
#Override
public void serialize(BigDecimal o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeNumber(o.toPlainString());
}
}
public static class Parameter {
#JsonSerialize(using = DefaultValueSerializer.class)
private BigDecimal prevValue;
public void setPrevValue(BigDecimal prevValue) {
this.prevValue = prevValue;
}
public BigDecimal getPrevValue() {
return prevValue;
}
}
}
It looks like this because com.fasterxml.jackson.core.JsonParser implementation used during deserialization (in your case conversion) process can not parse really big numbers. By default, number is treated as Long and value 3.9E38 exceeds it's range. Unfortunately, features USE_BIG_DECIMAL_FOR_FLOATS and USE_BIG_INTEGER_FOR_INTS are not handled in this case and we need to write customization here. See below example:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.util.JsonParserDelegate;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.deser.std.JsonNodeDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.io.IOException;
import java.math.BigDecimal;
public class JsonApp {
public static void main(String[] args) {
Parameter parameter = new Parameter();
parameter.setPrevValue(new BigDecimal("3.9E38"));
SimpleModule module = new SimpleModule();
module.addDeserializer(JsonNode.class, new BigDecimalFirstJsonNodeDeserializer());
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
JsonNode node = mapper.convertValue(parameter, JsonNode.class);
System.out.println(node);
}
}
class BigDecimalFirstJsonNodeDeserializer extends JsonNodeDeserializer {
#Override
public JsonNode deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return super.deserialize(new BigDecimalJsonParser(p), ctxt);
}
}
class BigDecimalJsonParser extends JsonParserDelegate {
public BigDecimalJsonParser(JsonParser parser) {
super(parser);
}
#Override
public NumberType getNumberType() {
return NumberType.BIG_DECIMAL;
}
#Override
public BigDecimal getDecimalValue() throws IOException {
String value = getText();
return new BigDecimal(value);
}
}
class BigDecimalPlainStringJsonSerializer extends JsonSerializer<BigDecimal> {
#Override
public void serialize(BigDecimal o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeNumber(o.toPlainString());
}
}
class Parameter {
#JsonSerialize(using = BigDecimalPlainStringJsonSerializer.class)
private BigDecimal prevValue;
public void setPrevValue(BigDecimal prevValue) {
this.prevValue = prevValue;
}
public BigDecimal getPrevValue() {
return prevValue;
}
}
Above code prints:
{"prevValue":3.9E+38}

How to invoke JsonSerializer inside another custom JsonSerializer Jackson

I have two classes. First with field of second class.
class A {
#JsonSerializer(using = CustomBSerializer.class)
private B b;
}
class B {
...
}
And I have two custom serializers:
class CustomBSerializer extends JsonSerializer<B> {
...
}
class CustomASerializer extends JsonSerializer<A> {
#Override
public void serialize(A a, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException {
jgen.writeStartObject();
//write here
jgen.writeEndObject();
}
}
What method I should add instead of comment in CustomASerializer to write serialized value of field b with CustomBSerializer?
You can use writeObjectField method. Jackson should use default or custom serializer if exists.
Below example:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.io.IOException;
import java.util.Date;
public class Main {
public static void main(String[] args) throws Exception {
B b = new B();
b.setProperty("Value");
A a = new A();
a.setB(b);
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(a));
}
}
#JsonSerialize(using = ASerializer.class)
class A {
private B b;
public B getB() {
return b;
}
public void setB(B b) {
this.b = b;
}
}
class ASerializer extends JsonSerializer<A> {
#Override
public void serialize(A value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
gen.writeStartObject();
gen.writeStringField("now", new Date().toString());
gen.writeObjectField("b", value.getB());
gen.writeEndObject();
}
}
#JsonSerialize(using = BSerializer.class)
class B {
private String property;
public String getProperty() {
return property;
}
public void setProperty(String property) {
this.property = property;
}
}
class BSerializer extends JsonSerializer<B> {
#Override
public void serialize(B value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
gen.writeStartObject();
gen.writeStringField("p", value.getProperty());
gen.writeEndObject();
}
}
prints:
{
"now" : "Wed Aug 26 22:27:08 CEST 2015",
"b" : {
"p" : "Value"
}
}
You don't always have access to class code, you will not always have the possibility to modify A or B class to put the annotation #JsonSerialize(using = BSerializer.class). In these cases you will need to call CustomBSerializer in CustomASerializer, which was also what the question was asking in first place. This is the way to do it:
class CustomASerializer extends JsonSerializer<A> {
#Override
public void serialize(A a, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
final ObjectMapper mapper = (ObjectMapper)jgen.getCodec();
SimpleModule module = new SimpleModule("CustomBSerializer");
module.addSerializer(B.class, new CustomBSerializer());
mapper.registerModule(module);
jgen.writeStartObject();
mapper.writeValue(jgen, a.getB());
jgen.writeEndObject();
}
}

How do I use a custom Serializer with Jackson?

I have two Java classes that I want to serialize to JSON using Jackson:
public class User {
public final int id;
public final String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
}
public class Item {
public final int id;
public final String itemNr;
public final User createdBy;
public Item(int id, String itemNr, User createdBy) {
this.id = id;
this.itemNr = itemNr;
this.createdBy = createdBy;
}
}
I want to serialize an Item to this JSON:
{"id":7, "itemNr":"TEST", "createdBy":3}
with User serialized to only include the id. I will also be able to serilize all user objects to JSON like:
{"id":3, "name": "Jonas", "email": "jonas#example.com"}
So I guess that I need to write a custom serializer for Item and tried with this:
public class ItemSerializer extends JsonSerializer<Item> {
#Override
public void serialize(Item value, JsonGenerator jgen,
SerializerProvider provider) throws IOException,
JsonProcessingException {
jgen.writeStartObject();
jgen.writeNumberField("id", value.id);
jgen.writeNumberField("itemNr", value.itemNr);
jgen.writeNumberField("createdBy", value.user.id);
jgen.writeEndObject();
}
}
I serialize the JSON with this code from Jackson How-to: Custom Serializers:
ObjectMapper mapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule("SimpleModule",
new Version(1,0,0,null));
simpleModule.addSerializer(new ItemSerializer());
mapper.registerModule(simpleModule);
StringWriter writer = new StringWriter();
try {
mapper.writeValue(writer, myItem);
} catch (JsonGenerationException e) {
e.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
But I get this error:
Exception in thread "main" java.lang.IllegalArgumentException: JsonSerializer of type com.example.ItemSerializer does not define valid handledType() (use alternative registration method?)
at org.codehaus.jackson.map.module.SimpleSerializers.addSerializer(SimpleSerializers.java:62)
at org.codehaus.jackson.map.module.SimpleModule.addSerializer(SimpleModule.java:54)
at com.example.JsonTest.main(JsonTest.java:54)
How can I use a custom Serializer with Jackson?
This is how I would do it with Gson:
public class UserAdapter implements JsonSerializer<User> {
#Override
public JsonElement serialize(User src, java.lang.reflect.Type typeOfSrc,
JsonSerializationContext context) {
return new JsonPrimitive(src.id);
}
}
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(User.class, new UserAdapter());
Gson gson = builder.create();
String json = gson.toJson(myItem);
System.out.println("JSON: "+json);
But I need to do it with Jackson now, since Gson doesn't have support for interfaces.
You can put #JsonSerialize(using = CustomDateSerializer.class) over any date field of object to be serialized.
public class CustomDateSerializer extends SerializerBase<Date> {
public CustomDateSerializer() {
super(Date.class, true);
}
#Override
public void serialize(Date value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException {
SimpleDateFormat formatter = new SimpleDateFormat("EEE MMM dd yyyy HH:mm:ss 'GMT'ZZZ (z)");
String format = formatter.format(value);
jgen.writeString(format);
}
}
As mentioned, #JsonValue is a good way. But if you don't mind a custom serializer, there's no need to write one for Item but rather one for User -- if so, it'd be as simple as:
public void serialize(Item value, JsonGenerator jgen,
SerializerProvider provider) throws IOException,
JsonProcessingException {
jgen.writeNumber(id);
}
Yet another possibility is to implement JsonSerializable, in which case no registration is needed.
As to error; that is weird -- you probably want to upgrade to a later version. But it is also safer to extend org.codehaus.jackson.map.ser.SerializerBase as it will have standard implementations of non-essential methods (i.e. everything but actual serialization call).
I tried doing this too, and there is a mistake in the example code on the Jackson web page that fails to include the type (.class) in the call to addSerializer() method, which should read like this:
simpleModule.addSerializer(Item.class, new ItemSerializer());
In other words, these are the lines that instantiate the simpleModule and add the serializer (with the prior incorrect line commented out):
ObjectMapper mapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule("SimpleModule",
new Version(1,0,0,null));
// simpleModule.addSerializer(new ItemSerializer());
simpleModule.addSerializer(Item.class, new ItemSerializer());
mapper.registerModule(simpleModule);
FYI: Here is the reference for the correct example code: http://wiki.fasterxml.com/JacksonFeatureModules
Use #JsonValue:
public class User {
int id;
String name;
#JsonValue
public int getId() {
return id;
}
}
#JsonValue only works on methods so you must add the getId method.
You should be able to skip your custom serializer altogether.
I wrote an example for a custom Timestamp.class serialization/deserialization, but you could use it for what ever you want.
When creating the object mapper do something like this:
public class JsonUtils {
public static ObjectMapper objectMapper = null;
static {
objectMapper = new ObjectMapper();
SimpleModule s = new SimpleModule();
s.addSerializer(Timestamp.class, new TimestampSerializerTypeHandler());
s.addDeserializer(Timestamp.class, new TimestampDeserializerTypeHandler());
objectMapper.registerModule(s);
};
}
for example in java ee you could initialize it with this:
import java.time.LocalDateTime;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
#Provider
public class JacksonConfig implements ContextResolver<ObjectMapper> {
private final ObjectMapper objectMapper;
public JacksonConfig() {
objectMapper = new ObjectMapper();
SimpleModule s = new SimpleModule();
s.addSerializer(Timestamp.class, new TimestampSerializerTypeHandler());
s.addDeserializer(Timestamp.class, new TimestampDeserializerTypeHandler());
objectMapper.registerModule(s);
};
#Override
public ObjectMapper getContext(Class<?> type) {
return objectMapper;
}
}
where the serializer should be something like this:
import java.io.IOException;
import java.sql.Timestamp;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
public class TimestampSerializerTypeHandler extends JsonSerializer<Timestamp> {
#Override
public void serialize(Timestamp value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
String stringValue = value.toString();
if(stringValue != null && !stringValue.isEmpty() && !stringValue.equals("null")) {
jgen.writeString(stringValue);
} else {
jgen.writeNull();
}
}
#Override
public Class<Timestamp> handledType() {
return Timestamp.class;
}
}
and deserializer something like this:
import java.io.IOException;
import java.sql.Timestamp;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.SerializerProvider;
public class TimestampDeserializerTypeHandler extends JsonDeserializer<Timestamp> {
#Override
public Timestamp deserialize(JsonParser jp, DeserializationContext ds) throws IOException, JsonProcessingException {
SqlTimestampConverter s = new SqlTimestampConverter();
String value = jp.getValueAsString();
if(value != null && !value.isEmpty() && !value.equals("null"))
return (Timestamp) s.convert(Timestamp.class, value);
return null;
}
#Override
public Class<Timestamp> handledType() {
return Timestamp.class;
}
}
These are behavior patterns I have noticed while trying to understand Jackson serialization.
1) Assume there is an object Classroom and a class Student. I've made everything public and final for ease.
public class Classroom {
public final double double1 = 1234.5678;
public final Double Double1 = 91011.1213;
public final Student student1 = new Student();
}
public class Student {
public final double double2 = 1920.2122;
public final Double Double2 = 2324.2526;
}
2) Assume that these are the serializers we use for serializing the objects into JSON. The writeObjectField uses the object's own serializer if it is registered with the object mapper; if not, then it serializes it as a POJO. The writeNumberField exclusively only accepts primitives as arguments.
public class ClassroomSerializer extends StdSerializer<Classroom> {
public ClassroomSerializer(Class<Classroom> t) {
super(t);
}
#Override
public void serialize(Classroom value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
jgen.writeStartObject();
jgen.writeObjectField("double1-Object", value.double1);
jgen.writeNumberField("double1-Number", value.double1);
jgen.writeObjectField("Double1-Object", value.Double1);
jgen.writeNumberField("Double1-Number", value.Double1);
jgen.writeObjectField("student1", value.student1);
jgen.writeEndObject();
}
}
public class StudentSerializer extends StdSerializer<Student> {
public StudentSerializer(Class<Student> t) {
super(t);
}
#Override
public void serialize(Student value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
jgen.writeStartObject();
jgen.writeObjectField("double2-Object", value.double2);
jgen.writeNumberField("double2-Number", value.double2);
jgen.writeObjectField("Double2-Object", value.Double2);
jgen.writeNumberField("Double2-Number", value.Double2);
jgen.writeEndObject();
}
}
3) Register only a DoubleSerializer with DecimalFormat output pattern ###,##0.000, in SimpleModule and the output is:
{
"double1" : 1234.5678,
"Double1" : {
"value" : "91,011.121"
},
"student1" : {
"double2" : 1920.2122,
"Double2" : {
"value" : "2,324.253"
}
}
}
You can see that the POJO serialization differentiates between double and Double, using the DoubleSerialzer for Doubles and using a regular String format for doubles.
4) Register DoubleSerializer and ClassroomSerializer, without the StudentSerializer. We expect that the output is such that if we write a double as an object, it behaves like a Double, and if we write a Double as a number, it behaves like a double. The Student instance variable should be written as a POJO and follow the pattern above since it does not register.
{
"double1-Object" : {
"value" : "1,234.568"
},
"double1-Number" : 1234.5678,
"Double1-Object" : {
"value" : "91,011.121"
},
"Double1-Number" : 91011.1213,
"student1" : {
"double2" : 1920.2122,
"Double2" : {
"value" : "2,324.253"
}
}
}
5) Register all serializers. The output is:
{
"double1-Object" : {
"value" : "1,234.568"
},
"double1-Number" : 1234.5678,
"Double1-Object" : {
"value" : "91,011.121"
},
"Double1-Number" : 91011.1213,
"student1" : {
"double2-Object" : {
"value" : "1,920.212"
},
"double2-Number" : 1920.2122,
"Double2-Object" : {
"value" : "2,324.253"
},
"Double2-Number" : 2324.2526
}
}
exactly as expected.
Another important note: If you have multiple serializers for the same class registered with the same Module, then the Module will select the serializer for that class that is most recently added to the list. This should not be used - it's confusing and I am not sure how consistent this is
Moral: if you want to customize serialization of primitives in your object, you must write your own serializer for the object. You cannot rely on the POJO Jackson serialization.
Jackson's JSON Views might be a simpler way of achieving your requirements, especially if you have some flexibility in your JSON format.
If {"id":7, "itemNr":"TEST", "createdBy":{id:3}} is an acceptable representation then this will be very easy to achieve with very little code.
You would just annotate the name field of User as being part of a view, and specify a different view in your serialisation request (the un-annotated fields would be included by default)
For example:
Define the views:
public class Views {
public static class BasicView{}
public static class CompleteUserView{}
}
Annotate the User:
public class User {
public final int id;
#JsonView(Views.CompleteUserView.class)
public final String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
}
And serialise requesting a view which doesn't contain the field you want to hide (non-annotated fields are serialised by default):
objectMapper.getSerializationConfig().withView(Views.BasicView.class);
In my case (Spring 3.2.4 and Jackson 2.3.1), XML configuration for custom serializer:
<mvc:annotation-driven>
<mvc:message-converters register-defaults="false">
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper">
<bean class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean">
<property name="serializers">
<array>
<bean class="com.example.business.serializer.json.CustomObjectSerializer"/>
</array>
</property>
</bean>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
was in unexplained way overwritten back to default by something.
This worked for me:
CustomObject.java
#JsonSerialize(using = CustomObjectSerializer.class)
public class CustomObject {
private Long value;
public Long getValue() {
return value;
}
public void setValue(Long value) {
this.value = value;
}
}
CustomObjectSerializer.java
public class CustomObjectSerializer extends JsonSerializer<CustomObject> {
#Override
public void serialize(CustomObject value, JsonGenerator jgen,
SerializerProvider provider) throws IOException,JsonProcessingException {
jgen.writeStartObject();
jgen.writeNumberField("y", value.getValue());
jgen.writeEndObject();
}
#Override
public Class<CustomObject> handledType() {
return CustomObject.class;
}
}
No XML configuration (<mvc:message-converters>(...)</mvc:message-converters>) is needed in my solution.
The problem in your case is the ItemSerializer is missing the method handledType() which needs to be overridden from JsonSerializer
public class ItemSerializer extends JsonSerializer<Item> {
#Override
public void serialize(Item value, JsonGenerator jgen,
SerializerProvider provider) throws IOException,
JsonProcessingException {
jgen.writeStartObject();
jgen.writeNumberField("id", value.id);
jgen.writeNumberField("itemNr", value.itemNr);
jgen.writeNumberField("createdBy", value.user.id);
jgen.writeEndObject();
}
#Override
public Class<Item> handledType()
{
return Item.class;
}
}
Hence you are getting the explicit error that handledType() is not defined
Exception in thread "main" java.lang.IllegalArgumentException: JsonSerializer of type com.example.ItemSerializer does not define valid handledType()
Hope it helps someone. Thanks for reading my answer.
If your only requirement in your custom serializer is to skip serializing the name field of User, mark it as transient. Jackson will not serialize or deserialize transient fields.
[ see also: Why does Java have transient fields? ]
You have to override method handledType and everything will work
#Override
public Class<Item> handledType()
{
return Item.class;
}

Categories