简体   繁体   English

Jackson:如何在不修改 POJO 的情况下将自定义属性添加到 JSON

[英]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.我正在为我的应用程序开发一个 REST 接口,使用 Jackson 将我的 POJO 域对象序列化为 JSON 表示。 I want to customize the serialization for some types to add additional properties to the JSON representation that do not exist in POJOs (eg add some metadata, reference data, etc).我想为某些类型自定义序列化,以向 JSON 表示添加 POJO 中不存在的其他属性(例如,添加一些元数据、引用数据等)。 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.我知道如何编写自己的JsonSerializer ,但在那种情况下,我需要为 object 的每个属性显式调用JsonGenerator.writeXXX(..)方法,而我只需要添加一个附加属性。 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, eg:或者(甚至更好)在jgen.writeEndObject()调用之前以某种方式拦截序列化,例如:

@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.我以为我可以扩展BeanSerializer并覆盖它的serialize(..)方法,但它被声明为final并且我找不到一种简单的方法来创建BeanSerializer的新实例而不向它提供所有类型元数据细节实际上复制了很大一部分Jackson。所以我已经放弃了。

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.我的问题是- 如何自定义 Jackson 的序列化以向特定 POJO 的 JSON output 添加额外的内容,而不引入过多的样板代码并尽可能多地重用默认的 Jackson 行为。

Since (I think) Jackson 1.7 you can do this with a BeanSerializerModifier and extending BeanSerializerBase . 从(我认为)Jackson 1.7开始,您可以使用BeanSerializerModifier并扩展BeanSerializerBase I've tested the example below with Jackson 2.0.4. 我用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();
        }
    }
}

Jackson 2.5 introduced the @JsonAppend annotation, which can be used to add "virtual" properties during serialization. Jackson 2.5引入了@JsonAppend注释,可用于在序列化期间添加“虚拟”属性。 It can be used with the mixin functionality to avoid modifying the original POJO. 它可以与mixin功能一起使用,以避免修改原始POJO。

The following example adds an ApprovalState property during serialization: 以下示例在序列化期间添加ApprovalState属性:

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

Register the mixin with the ObjectMapper : 使用ObjectMapper注册mixin:

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

Use an ObjectWriter to set the attribute during serialization: 在序列化期间使用ObjectWriter设置属性:

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

Using the writer for serialization will add the ApprovalState field to the ouput. 使用编写器进行序列化将将ApprovalState字段添加到输出。

You can do this (previous version did not work with Jackson after 2.6, but this works with Jackson 2.7.3): 你可以这样做(以前的版本在2.6之后不适用于Jackson,但这适用于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. 我用杰克逊2.9.0和2.9.6尝试了它,并且两者都按预期工作。 Perhaps try this out: http://jdoodle.com/a/z99 (run it locally - jdoodle apparently can't handle Jackson). 也许试试这个: http ://jdoodle.com/a/z99(在本地运行 - jdoodle显然无法处理杰克逊)。

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

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: 在我为所有“Jackson Pojos”提供的基础课中,我补充说:

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. 我现在可以反序列化到Pojo,使用字段并且可以在失去任何属性的情况下进行重新序列化。 I can also add/change non pojo properties: 我还可以添加/更改非pojo属性:

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

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

(Got it from Cowtowncoder ) (来自Cowtowncoder

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

        }

Another and perhaps the most simple solution: 另一种也许是最简单的解决方案:

Make serialisation a 2-step process. 使序列化分为两步。 First create a Map<String,Object> like: 首先创建一个Map<String,Object>如:

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: 然后将其序列化为json:

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

Inspired from what wajda said and written in this gist : 灵感来自于wajda在这个要点中所说和写的内容:

Here is how to add a listener for bean serialization in jackson 1.9.12. 以下是如何在jackson 1.9.12中为bean序列化添加监听器。 In this example, the listerner is considered as a Chain Of Command which interface is : 在此示例中,listerner被视为命令链,其接口是:

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

MyBeanSerializer.java: 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: 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: 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: 下面的最后一节显示了如何使用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();
    }
}

We can extend BeanSerializer , but with little trick. 我们可以扩展BeanSerializer ,但只需要一点点技巧。

First, define a java class to wrapper your POJO. 首先,定义一个java类来包装你的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 . 然后,实现自定义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注释来自定义序列化行为的原始对象的情况。

I needed this ability as well; 我也需要这种能力; in my case, to support field expansion on REST services. 就我而言,支持REST服务的字段扩展。 I ended up developing a tiny framework to solve this problem, and it's open sourced on github . 我最终开发了一个小框架来解决这个问题,它是在github上开源的。 It's also available in the maven central repository . 它也可以在maven中央存储库中使用

It takes care of all the work. 它负责所有的工作。 Simply wrap the POJO in a MorphedResult, and then add or remove properties at will. 只需将POJO包装在MorphedResult中,然后随意添加或删除属性。 When serialized, the MorphedResult wrapper disappears and any 'changes' appear in the serialized JSON object. 序列化时,MorphedResult包装器消失,并且序列化的JSON对象中出现任何“更改”。

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

See the github page for more details and examples. 有关更多详细信息和示例,请参阅github页面。 Be sure to register the libraries 'filter' with Jackson's object mapper like so: 请务必使用Jackson的对象映射器注册库的“过滤器”,如下所示:

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

This google groups thread points to the BeanSerializerModifier.changeProperties method: https://groups.google.com/g/jackson-user/c/uYIxbRZhsIM/m/1QpLh7G72C0J这个 google groups 线程指向 BeanSerializerModifier.changeProperties 方法: https://groups.google.com/g/jackson-user/c/uYIxbRZhsIM/m/1QpLh7G72C0J

It looks like this method makes the least interference with the object serialization, which is very convenient if you have other serialization customizations.貌似这个方法对object序列化的干扰最小,如果你有其他序列化定制的话很方便。

You can add more objects to the given beanProperties list.您可以将更多对象添加到给定的 beanProperties 列表中。

Suppose, we have this bean to be serialized:假设,我们要序列化这个 bean:

public class MyClass {

    private final String name;
    private final String description;

    public MyClass(String name, String description) {
        this.name = name;
        this.description = description;
    }

    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    public String getName() {
        return name;
    }

    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    public String getDescription() {
        return description;
    }
}

Then you can add a SerializerModifier to your ObjectMapper instance.然后您可以将 SerializerModifier 添加到您的 ObjectMapper 实例。 The most interesting parts are the MyBeanSerializerModifier.changeProperties and the CustomPropertyWriter.value methods.最有趣的部分是 MyBeanSerializerModifier.changeProperties 和 CustomPropertyWriter.value 方法。

private void addSerializationCustomization(ObjectMapper objectMapper,
                                           SomeAdditionalDataFactory dataFactory) {
    SimpleModule module = new SimpleModule();
    BeanSerializerModifier modifier = new MyBeanSerializerModifier(dataFactory);
    module.setSerializerModifier(modifier);
    objectMapper.registerModule(module);
}

private static class MyBeanSerializerModifier extends BeanSerializerModifier {

    private final SomeAdditionalDataFactory dataFactory;

    public MyBeanSerializerModifier(SomeAdditionalDataFactory dataFactory) {
        this.dataFactory = dataFactory;
    }

    @Override
    public List<BeanPropertyWriter> changeProperties(SerializationConfig config,
                                                     BeanDescription beanDesc,
                                                     List<BeanPropertyWriter> beanProperties) {
        if (MyClass.class.isAssignableFrom(beanDesc.getBeanClass())) {
            Map<String, Function<MyClass, String>> additionalFields = Map.of(
                    "someData1",
                    myObj -> dataFactory.getSomeData1(myObj),
                    "someData2",
                    myObj -> dataFactory.getSomeData2(myObj),
                    "someData3",
                    myObj -> dataFactory.getSomeData3(myObj)
            );
            JavaType javaType = SimpleType.constructUnsafe(String.class);

            for (Map.Entry<String, Function<MyClass, String>> entry : additionalFields.entrySet()) {
                VirtualAnnotatedMember member = new VirtualAnnotatedMember(
                        null, beanDesc.getBeanClass(), entry.getKey(), javaType);
                BeanPropertyDefinition definition = SimpleBeanPropertyDefinition
                    .construct(config, member, new PropertyName(entry.getKey()));
                BeanPropertyWriter writer = new CustomPropertyWriter<>(
                    definition, javaType, entry.getValue());
                beanProperties.add(writer);
            }
        }
        return super.changeProperties(config, beanDesc, beanProperties);
    }
}

private static class CustomPropertyWriter<T> extends VirtualBeanPropertyWriter {

    private final Function<T, String> getter;

    public CustomPropertyWriter(BeanPropertyDefinition propDef,
                                JavaType declaredType,
                                Function<T, String> getter) {
        super(propDef, null, declaredType);
        this.getter = getter;
    }

    @Override
    @SuppressWarnings("unchecked")
    protected Object value(Object bean,
                           JsonGenerator gen,
                           SerializerProvider prov) throws Exception {
        return getter.apply((T) bean);
    }

    @Override
    public VirtualBeanPropertyWriter withConfig(MapperConfig<?> config,
                                                AnnotatedClass declaringClass,
                                                BeanPropertyDefinition propDef,
                                                JavaType type) {
        throw new IllegalStateException("Should not be called on this type");
    }
}

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: 在详细了解Jackson源代码后,我得出结论,如果不编写我自己的BeanSerializerBeanSerializerBuilderBeanSerializerFactory并提供一些扩展点,就完全不可能实现:

/*
/**********************************************************
/* 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 不幸的是,我不得不将整个JacksonBeanSerializer源代码复制并粘贴到MyCustomBeanSerializer因为前者不是为扩展而开发的,用于声明所有字段和一些重要方法(如serialize(...) )作为final

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM