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

Solution 1:

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.

Solution 2:

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();
        }
    }
}

Solution 3:

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"
} ]