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

Posted

技术标签:

【中文标题】Jackson:如何在不修改 POJO 的情况下向 JSON 添加自定义属性【英文标题】:Jackson: How to add custom property to the JSON without modifying the POJO 【发布时间】:2013-01-20 18:44:41 【问题描述】:

我正在为我的应用程序开发一个 REST 接口,使用 Jackson 将我的 POJO 域对象序列化为 JSON 表示。我想为某些类型自定义序列化,以向 POJO 中不存在的 JSON 表示添加其他属性(例如,添加一些元数据、参考数据等)。我知道如何编写自己的 JsonSerializer,但在这种情况下,我需要为对象的 each 属性显式调用 JsonGenerator.writeXXX(..) 方法,而我只需要 add 附加属性。换句话说,我希望能够写出类似的东西:

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

或者(甚至更好)在jgen.writeEndObject()调用之前以某种方式拦截序列化,例如:

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

我以为我可以扩展 BeanSerializer 并覆盖它的 serialize(..) 方法,但它被声明为 final,而且我找不到一种简单的方法来创建 BeanSerializer 的新实例而不提供所有类型元数据细节实际上复制了杰克逊的大部分内容。所以我已经放弃了。

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

【问题讨论】:

由于Jackson-2.5 JsonAppend注解可以解决这个问题。见@Henrik answer below 【参考方案1】:

您可以这样做(以前的版本在 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();
        
    

更新:

我在 Jackson 2.9.0 和 2.9.6 上进行了尝试,两者都按预期工作。也许试试这个:http://jdoodle.com/a/z99(在本地运行它 - jdoodle 显然无法处理 Jackson)。

【讨论】:

它有效,但我不明白你为什么在findValueSerializerunwrappingSerializer 方法中使用null @herau:第一个 null 表示我正在序列化一个根对象。第二个 null 是因为我不想应用名称转换。 我试过了,但是 findValueSerializer 总是返回相同的 CustomClassSerializer,所以它进入了无限递归。此外,findValueSerializer 不适用于 'value',因为它需要类型,而不是 Ojbect。 @DavidA:Jackson 2.5 的代码被破坏了,但我添加了一个适用于 Jackson 2.7 的固定版本。 @LasseJacobs 我在 Jackson 2.9.0 和 2.9.6 上进行了尝试,两者都按预期工作。也许试试这个:jdoodle.com/a/z99(在本地运行它 - jdoodle 显然无法处理杰克逊)。【参考方案2】:

对于我的用例,我可以使用更简单的方法。在我为所有“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);

我现在可以反序列化为 Pojo,处理字段并重新序列化,但不会丢失任何属性。我还可以添加/更改非 pojo 属性:

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

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

(从Cowtowncoder得到它)

【讨论】:

【参考方案3】:

另一个也许是最简单的解决方案:

使序列化成为一个两步过程。首先创建一个Map&lt;String,Object&gt;like:

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

然后添加你想要的属性:

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

然后将其序列化为 json:

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

【讨论】:

【参考方案4】:

Jackson 2.5 引入了@JsonAppend 注解,可用于在序列化过程中添加“虚拟”属性。它可以与mixin功能一起使用,以避免修改原始POJO。

以下示例在序列化过程中添加ApprovalState 属性:

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

使用ObjectMapper注册mixin:

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

在序列化期间使用ObjectWriter 设置属性:

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

使用编写器进行序列化会将ApprovalState 字段添加到输出中。

【讨论】:

这是问题的实际答案。看起来像杰克逊的做法! 这解决了问题的特定方面,但不能解决问题本身。就我而言,我需要用变量的值包装一个对象,这意味着注释不起作用。 (类似于 OP 的示例代码,如 startObject(); fieldName(myVariable);allFields(obj); endObject();endObject(); 是我需要的)。【参考方案5】:

我们可以扩展BeanSerializer,但需要一点小技巧。

首先,定义一个 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;
    


然后,实现你的自定义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);
        

    


这样,我们可以处理原始对象使用杰克逊注解来自定义序列化行为的情况。

【讨论】:

【参考方案6】:

虽然已经回答了这个问题,但我找到了另一种不需要特殊 Jackson 挂钩的方法。

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

输出:


  "baseField" : "inner",
  "extraField" : "outer"

要编写集合,您可以简单地使用视图:

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

输出:

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

【讨论】:

如果使用 Kotlin,请使用 @get:JsonUnwrapped 注释要解包的属性。 这是最好的答案——人们不应该仅仅因为涉及 JSON 就忘记视图模型。【参考方案7】:

灵感来自 wajda 在 gist 中所说和所写的内容:

下面是在jackson 1.9.12 中如何为bean 序列化添加监听器。在这个例子中,listerner 被认为是一个命令链,它的接口是:

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

下面的最后一课展示了如何使用 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();
    

【讨论】:

【参考方案8】:

我也需要这种能力;就我而言,支持 REST 服务的字段扩展。我最终开发了一个小框架来解决这个问题,它在github 上开源。它也可以在maven central repository 中找到。

它负责所有工作。只需将 POJO 包装在一个 MorphedResult 中,然后随意添加或删除属性。序列化后,MorphedResult 包装器会消失,并且任何“更改”都会出现在序列化的 JSON 对象中。

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

有关更多详细信息和示例,请参阅 github 页面。请务必使用 Jackson 的对象映射器注册库“过滤器”,如下所示:

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

【讨论】:

【参考方案9】:

我们可以使用反射来获取要解析的对象的所有字段。

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

在自定义序列化器中,我们有这样的序列化方法:

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

        

【讨论】:

Field 类来自import java.lang.reflect.Field; 如果您在域对象中使用@JsonProperty(value="someOtherName") 或@JsonIgnore 会怎样?通过反射,您可以覆盖现有的杰克逊功能。这似乎不太好。【参考方案10】:

从(我认为)Jackson 1.7 开始,您可以使用 BeanSerializerModifier 并扩展 BeanSerializerBase 来做到这一点。我已经用 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();
        
    

【讨论】:

确认它适用于 2.0.4。在 2.4.1 中,有两个新的抽象方法要实现(只是从 BeanSerializer 复制),并且 modifySerializer 也得到一个 StringSerializer,它不能强制转换。因此,您必须在转换为 BeanSerializerBase 之前进行 instanceof 检查 我认为人们应该知道杰克逊的“虚拟财产”功能是从 2.5 开始出现的。此功能已在 answer below 中进行了说明【参考方案11】:

在查看了 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

不幸的是,我不得不将整个 JacksonBeanSerializer 源代码复制并粘贴到 MyCustomBeanSerializer,因为前者不是为声明所有字段和一些重要方法的扩展而开发的(例如 @987654328 @) 为final

【讨论】:

对 Code-Nazis 的 final 使用不当。我经常因为最终方法或类而无法扩展现有代码。不要与性能争论:***.com/questions/4279420/… @Dag 我不会在那里妄下结论。如果开发人员选择进行类决赛,那么这很可能是一个经过深思熟虑的决定。为扩展开设课程是一个不应该轻易做出的决定。

以上是关于Jackson:如何在不修改 POJO 的情况下向 JSON 添加自定义属性的主要内容,如果未能解决你的问题,请参考以下文章

在不修改 config.ini 的情况下向已安装的 eclipse rcp 软件添加插件

Jackson 使用枚举键、POJO 值反序列化为 Map

如何在不阻止 textView 触摸的情况下向所有 UITextView 添加一个 UIGestureRecognizer

在没有TypeInfo的情况下向Jackson提供自定义对象实例

如何在不复制对象的情况下向 Python 公开返回 C++ 对象的函数?

如何在不覆盖的情况下向 Firebase Firestore 添加值?