Jackson: How to add custom property to the JSON without modifying the POJO

I am developing a REST interface for my app using Jackson to serialize my POJO domain objects to JSON representation. I want to customize the serialization for some types to add additional properties to the JSON representation that do not exist in POJOs (e.g. add some metadata, reference data, etc). I know how to write my own JsonSerializer, but in that case I would need to explicitly call JsonGenerator.writeXXX(..) methods for each property of my object while all I need is just to add an additional property. In other words I would like to be able to write something like:

@Override
public void serialize(TaxonomyNode value, JsonGenerator jgen, SerializerProvider provider) {
jgen.writeStartObject();
jgen.writeAllFields(value); // <-- The method I'd like to have
jgen.writeObjectField("my_extra_field", "some data");
jgen.writeEndObject();
}

or (even better) to somehow intercept the serialization before the jgen.writeEndObject() call, e.g.:

@Override void beforeEndObject(....) {
jgen.writeObjectField("my_extra_field", "some data");
}

I thought I could extend BeanSerializer and override its serialize(..) method but it's declared final and also I couldn't find an easy way to create a new instance of BeanSerializer without providing it with all the type metadata details practically duplicating a good portion of Jackson. So I've given up on doing that.

My question is - how to customize Jackson's serialization to add additional stuff to the JSON output for particular POJOs without introducing too much of the boilerplate code and reusing as much as possible of the default Jackson behaviour.

82104 次浏览

After looking more on the Jackson source code I concluded that it's simply impossible to achieve without writing my own BeanSerializer, BeanSerializerBuilder and BeanSerializerFactory and provide some extension points like:

/*
/**********************************************************
/* Extension points
/**********************************************************
*/


protected void beforeEndObject(T bean, JsonGenerator jgen, SerializerProvider provider) throws IOException, JSONException {
// May be overridden
}


protected void afterStartObject(T bean, JsonGenerator jgen, SerializerProvider provider) throws IOException, JSONException {
// May be overridden
}

Unfortunately I had to copy and paste entire Jackson's BeanSerializer source code to MyCustomBeanSerializer because the former is not developed for extensions declaring all the fields and some important methods (like serialize(...)) as final

Since (I think) Jackson 1.7 you can do this with a BeanSerializerModifier and extending BeanSerializerBase. I've tested the example below with Jackson 2.0.4.

import java.io.IOException;


import org.junit.Test;


import com.fasterxml.jackson.core.JsonGenerationException;
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 com.fasterxml.jackson.databind.ser.impl.ObjectIdWriter;
import com.fasterxml.jackson.databind.ser.std.BeanSerializerBase;




public class JacksonSerializeWithExtraField {


@Test
public void testAddExtraField() throws Exception
{
ObjectMapper mapper = new ObjectMapper();


mapper.registerModule(new SimpleModule() {


public void setupModule(SetupContext context) {
super.setupModule(context);


context.addBeanSerializerModifier(new BeanSerializerModifier() {


public JsonSerializer<?> modifySerializer(
SerializationConfig config,
BeanDescription beanDesc,
JsonSerializer<?> serializer) {
if (serializer instanceof BeanSerializerBase) {
return new ExtraFieldSerializer(
(BeanSerializerBase) serializer);
}
return serializer;


}
});
}
});


mapper.writeValue(System.out, new MyClass());
//prints {"classField":"classFieldValue","extraField":"extraFieldValue"}
}




class MyClass {


private String classField = "classFieldValue";


public String getClassField() {
return classField;
}
public void setClassField(String classField) {
this.classField = classField;
}
}




class ExtraFieldSerializer extends BeanSerializerBase {


ExtraFieldSerializer(BeanSerializerBase source) {
super(source);
}


ExtraFieldSerializer(ExtraFieldSerializer source,
ObjectIdWriter objectIdWriter) {
super(source, objectIdWriter);
}


ExtraFieldSerializer(ExtraFieldSerializer source,
String[] toIgnore) {
super(source, toIgnore);
}


protected BeanSerializerBase withObjectIdWriter(
ObjectIdWriter objectIdWriter) {
return new ExtraFieldSerializer(this, objectIdWriter);
}


protected BeanSerializerBase withIgnorals(String[] toIgnore) {
return new ExtraFieldSerializer(this, toIgnore);
}


public void serialize(Object bean, JsonGenerator jgen,
SerializerProvider provider) throws IOException,
JsonGenerationException {
jgen.writeStartObject();
serializeFields(bean, jgen, provider);
jgen.writeStringField("extraField", "extraFieldValue");
jgen.writeEndObject();
}
}
}

You can do this (previous version did not work with Jackson after 2.6, but this works with Jackson 2.7.3):

public static class CustomModule extends SimpleModule {
public CustomModule() {
addSerializer(CustomClass.class, new CustomClassSerializer());
}


private static class CustomClassSerializer extends JsonSerializer {
@Override
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
//Validate.isInstanceOf(CustomClass.class, value);
jgen.writeStartObject();
JavaType javaType = provider.constructType(CustomClass.class);
BeanDescription beanDesc = provider.getConfig().introspect(javaType);
JsonSerializer<Object> serializer = BeanSerializerFactory.instance.findBeanSerializer(provider,
javaType,
beanDesc);
// this is basically your 'writeAllFields()'-method:
serializer.unwrappingSerializer(null).serialize(value, jgen, provider);
jgen.writeObjectField("my_extra_field", "some data");
jgen.writeEndObject();
}
}
}

Update:

I tried it out with Jackson 2.9.0 and 2.9.6 and it worked as expected with both. Perhaps try this out: http://jdoodle.com/a/z99 (run it locally - jdoodle apparently can't handle Jackson).

We can use reflection to get all the fields of the object you want to parse.

@JsonSerialize(using=CustomSerializer.class)
class Test{
int id;
String name;
String hash;
}

In custom serializer, we have our serialize method like this :

        @Override
public void serialize(Test value, JsonGenerator jgen,
SerializerProvider provider) throws IOException,
JsonProcessingException {


jgen.writeStartObject();
Field[] fields = value.getClass().getDeclaredFields();


for (Field field : fields) {
try {
jgen.writeObjectField(field.getName(), field.get(value));
} catch (IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}


}
jgen.writeObjectField("extra_field", "whatever_value");
jgen.writeEndObject();


}

I needed this ability as well; in my case, to support field expansion on REST services. I ended up developing a tiny framework to solve this problem, and it's open sourced on github. It's also available in the maven central repository.

It takes care of all the work. Simply wrap the POJO in a MorphedResult, and then add or remove properties at will. When serialized, the MorphedResult wrapper disappears and any 'changes' appear in the serialized JSON object.

MorphedResult<?> result = new MorphedResult<>(pojo);
result.addExpansionData("my_extra_field", "some data");

See the github page for more details and examples. Be sure to register the libraries 'filter' with Jackson's object mapper like so:

ObjectMapper mapper = new ObjectMapper();
mapper.setFilters(new FilteredResultProvider());

Inspired from what wajda said and written in this gist:

Here is how to add a listener for bean serialization in jackson 1.9.12. In this example, the listerner is considered as a Chain Of Command which interface is :

public interface BeanSerializerListener {
void postSerialization(Object value, JsonGenerator jgen) throws IOException;
}

MyBeanSerializer.java:

public class MyBeanSerializer extends BeanSerializerBase {
private final BeanSerializerListener serializerListener;


protected MyBeanSerializer(final BeanSerializerBase src, final BeanSerializerListener serializerListener) {
super(src);
this.serializerListener = serializerListener;
}


@Override
public void serialize(final Object bean, final JsonGenerator jgen, final SerializerProvider provider) throws IOException, JsonGenerationException {
jgen.writeStartObject();
if (_propertyFilterId != null) {
serializeFieldsFiltered(bean, jgen, provider);
} else {
serializeFields(bean, jgen, provider);
}


serializerListener.postSerialization(bean, jgen);


jgen.writeEndObject();
}
}

MyBeanSerializerBuilder.java:

public class MyBeanSerializerBuilder extends BeanSerializerBuilder {
private final BeanSerializerListener serializerListener;


public MyBeanSerializerBuilder(final BasicBeanDescription beanDesc, final BeanSerializerListener serializerListener) {
super(beanDesc);
this.serializerListener = serializerListener;
}


@Override
public JsonSerializer<?> build() {
BeanSerializerBase src = (BeanSerializerBase) super.build();
return new MyBeanSerializer(src, serializerListener);
}
}

MyBeanSerializerFactory.java:

public class MyBeanSerializerFactory extends BeanSerializerFactory {


private final BeanSerializerListener serializerListener;


public MyBeanSerializerFactory(final BeanSerializerListener serializerListener) {
super(null);
this.serializerListener = serializerListener;
}


@Override
protected BeanSerializerBuilder constructBeanSerializerBuilder(final BasicBeanDescription beanDesc) {
return new MyBeanSerializerBuilder(beanDesc, serializerListener);
}
}

The last class below shows how to provide it using Resteasy 3.0.7:

@Provider
public class ObjectMapperProvider implements ContextResolver<ObjectMapper> {
private final MapperConfigurator mapperCfg;


public ObjectMapperProvider() {
mapperCfg = new MapperConfigurator(null, null);
mapperCfg.setAnnotationsToUse(new Annotations[]{Annotations.JACKSON, Annotations.JAXB});
mapperCfg.getConfiguredMapper().setSerializerFactory(serializerFactory);
}


@Override
public ObjectMapper getContext(final Class<?> type) {
return mapperCfg.getConfiguredMapper();
}
}

Though this question is already answered, I found another way that requires no special Jackson hooks.

static class JsonWrapper<T> {
@JsonUnwrapped
private T inner;
private String extraField;


public JsonWrapper(T inner, String field) {
this.inner = inner;
this.extraField = field;
}


public T getInner() {
return inner;
}


public String getExtraField() {
return extraField;
}
}


static class BaseClass {
private String baseField;


public BaseClass(String baseField) {
this.baseField = baseField;
}


public String getBaseField() {
return baseField;
}
}


public static void main(String[] args) throws JsonProcessingException {
Object input = new JsonWrapper<>(new BaseClass("inner"), "outer");
System.out.println(new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(input));
}

Outputs:

{
"baseField" : "inner",
"extraField" : "outer"
}

For writing collections, you can simply use a view:

public static void main(String[] args) throws JsonProcessingException {
List<BaseClass> inputs = Arrays.asList(new BaseClass("1"), new BaseClass("2"));
//Google Guava Library <3
List<JsonWrapper<BaseClass>> modInputs = Lists.transform(inputs, base -> new JsonWrapper<>(base, "hello"));
System.out.println(new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(modInputs));
}

Output:

[ {
"baseField" : "1",
"extraField" : "hello"
}, {
"baseField" : "2",
"extraField" : "hello"
} ]

We can extend BeanSerializer, but with little trick.

First, define a java class to wrapper your POJO.

@JsonSerialize(using = MixinResultSerializer.class)
public class MixinResult {


private final Object origin;
private final Map<String, String> mixed = Maps.newHashMap();


@JsonCreator
public MixinResult(@JsonProperty("origin") Object origin) {
this.origin = origin;
}


public void add(String key, String value) {
this.mixed.put(key, value);
}


public Map<String, String> getMixed() {
return mixed;
}


public Object getOrigin() {
return origin;
}


}

Then,implement your custom serializer.

public final class MixinResultSerializer extends BeanSerializer {


public MixinResultSerializer() {
super(SimpleType.construct(MixinResult.class), null, new BeanPropertyWriter[0], new BeanPropertyWriter[0]);
}


public MixinResultSerializer(BeanSerializerBase base) {
super(base);
}


@Override
protected void serializeFields(Object bean, JsonGenerator gen, SerializerProvider provider) throws IOException {
if (bean instanceof MixinResult) {
MixinResult mixin  = (MixinResult) bean;
Object      origin = mixin.getOrigin();


BeanSerializer serializer = (BeanSerializer) provider.findValueSerializer(SimpleType.construct(origin.getClass()));


new MixinResultSerializer(serializer).serializeFields(origin, gen, provider);


mixin.getMixed().entrySet()
.stream()
.filter(entry -> entry.getValue() != null)
.forEach((entry -> {
try {
gen.writeFieldName(entry.getKey());
gen.writeRawValue(entry.getValue());
} catch (IOException e) {
throw new RuntimeException(e);
}
}));
} else {
super.serializeFields(bean, gen, provider);
}


}


}

This way, we can handle the case that origin object using jackson annotations to custom serialize behavior.

Jackson 2.5 introduced the @JsonAppend annotation, which can be used to add "virtual" properties during serialization. It can be used with the mixin functionality to avoid modifying the original POJO.

The following example adds an ApprovalState property during serialization:

@JsonAppend(
attrs = {
@JsonAppend.Attr(value = "ApprovalState")
}
)
public static class ApprovalMixin {}

Register the mixin with the ObjectMapper:

mapper.addMixIn(POJO.class, ApprovalMixin.class);

Use an ObjectWriter to set the attribute during serialization:

ObjectWriter writer = mapper.writerFor(POJO.class)
.withAttribute("ApprovalState", "Pending");

Using the writer for serialization will add the ApprovalState field to the ouput.

Another and perhaps the most simple solution:

Make serialisation a 2-step process. First create a Map<String,Object> like:

Map<String,Object> map = req.mapper().convertValue( result, new TypeReference<Map<String,Object>>() {} );

then add the properties you want like:

map.put( "custom", "value" );

then serialise this to json:

String json = req.mapper().writeValueAsString( map );

For my use case, I could use a much simpler way. In a the base class I have for all my "Jackson Pojos" I add:

protected Map<String,Object> dynamicProperties = new HashMap<String,Object>();


...




public Object get(String name) {
return dynamicProperties.get(name);
}


// "any getter" needed for serialization
@JsonAnyGetter
public Map<String,Object> any() {
return dynamicProperties;
}


@JsonAnySetter
public void set(String name, Object value) {
dynamicProperties.put(name, value);
}

I can now deserialize to Pojo, work with fields and reserialize witjout losing any properties. I can also add/change non pojo properties:

// Pojo fields
person.setFirstName("Annna");


// Dynamic field
person.set("ex", "test");

(Got it from Cowtowncoder)