杰克逊:对于某种类型的所有属性,不同的JsonInclude(例如,可选)

时间:2017-01-23 17:15:36

标签: java serialization java-8 jackson optional

在我的Java应用程序中使用Jackson进行序列化(POJO到JSON)和反序列化(JSON到POJO)时,我通常希望保留所有字段,因此使用(默认)JsonInclude.Value.ALWAYS

为了允许通过应用程序的Rest API进行部分更新,我还想区分特定设置为null的值和保持不变的值。为此,Java8 Optional<?>类似乎是正确的选择。

要获得适当的支持,我必须将Jdk8Module添加到ObjectMapper。一切都很简单。

开箱即用的反序列化行为正是我想要的。不存在的字段仍为默认字段(此处为null),明确提供的null值已反序列化为Optional.empty()(或Optional.ofNullable(null))。

我想要的是Optional<?>字段,其显式值null将从生成的JSON中排除,但任何其他字段(例如普通Integer)始终包含(即使它是null)。

可用的众多选项之一是MixIn。不幸的是,MixIn可能适用于其他注释,但不适用于@JsonInclude(这似乎是杰克逊的一个错误)。

public class OptionalObjectMappingTest {
    public static class MyBean {
        public Integer one;
        public Optional<Integer> two;
        public Optional<Integer> three;
        public Optional<Integer> four;
    }

    @JsonInclude(JsonInclude.Include.NOT_NULL)
    public static class OptionalMixIn {}

    private ObjectMapper initObjectMapper() {
        return new ObjectMapper()
                .registerModule(new Jdk8Module())
                .setSerialisationInclusion(JsonInclude.Include.ALWAYS)
                .addMixIn(Optional.class, OptionalMixIn.class);
    }

    @Test
    public void testDeserialisation() {
        String json = "{\"one\":null,\"two\":2,\"three\":null}";
        MyBean bean = initObjectMapper().readValue(json, MyBean.class);
        Assert.assertNull(bean.one);
        Assert.assertEquals(Optional.of(2), bean.two);
        Assert.assertEquals(Optional.empty(), bean.three);
        Assert.assertNull(bean.four);
    }

    @Test
    public void testSerialisation() {
        MyBean bean = new MyBean();
        bean.one = null;
        bean.two = Optional.of(2);
        bean.three = Optional.empty();
        bean.four = null;
        String result = initObjectMapper().writeValueAsString(bean);
        String expected = "{\"one\":null,\"two\":2,\"three\":null}";
        // FAILS, due to result = "{one:null,two:2,three:null,four:null}"
        Assert.assertEquals(expected, result);
    }
}

有很多方法可以(动态地)包含/排除字段及其值,但没有一种方法可以包含/排除#34;官方&#34;似乎可以做到这一点:

    每个@JsonInclude字段上的
  1. Optional<?>注释实际上都是我想要的,但这太容易忘记和麻烦。
  2. 自定义MixIn应该允许每种类型的JsonInclude注释的全局定义,但显然不适用(按照上面的示例测试)。
  3. @JsonIgnore(和相关的)注释是静态的,并不关心字段的价值。
  4. 需要在包含@JsonFilter字段的每个类上设置
  5. Optional,您需要知道PropertyFilter中每个受影响的类型。即甚至不仅仅是在每个JsonInclude字段上添加Optional
  6. @JsonView不允许根据给定的bean实例的字段值动态包含/排除字段。
  7. 通过JsonSerializer<?>注册的自定义ObjectMapper.setNullValueSerializer()仅在插入字段名称后调用,即如果我们不做任何操作,生成的JSON无效。
  8. 在将字段的名称插入JSON之前,涉及自定义BeanSerializerModifier,但它无法访问字段的值。

2 个答案:

答案 0 :(得分:1)

  编辑:      

根据StaxMan的回答,这似乎不是一个错误,而是一个功能,因为混合并不意味着为某种类型的每个属性添加注释。如问题中所述使用混合的尝试只会在@JsonInclude类上添加Optional注释,该注释具有不同的含义(已在其他答案中描述)。

jackson-databind 版本2.9.0

的解决方案

由于混合设计的行为方式不同,因此ObjectMapper configOverride()上有一个新的配置选项:

  • setIncludeAsProperty()

配置非常简单:

private ObjectMapper initObjectMapper() {
    ObjectMapper objectMapper = new ObjectMapper()
            .registerModule(new Jdk8Module())
            .setSerializationInclusion(JsonInclude.Include.ALWAYS);
    objectMapper.configOverride(Optional.class)
            .setIncludeAsProperty(JsonInclude.Value
                    .construct(JsonInclude.Include.NON_NULL, null));
    return objectMapper;
}

调整后的示例如下所示:

public class OptionalObjectMappingTest {

    public static class MyBean {
        public Integer one;
        public Optional<Integer> two;
        public Optional<Integer> three;
        public Optional<Integer> four;
    }

    private ObjectMapper initObjectMapper() {
        ObjectMapper objectMapper = new ObjectMapper()
                .registerModule(new Jdk8Module())
                .setSerializationInclusion(JsonInclude.Include.ALWAYS);
        objectMapper.configOverride(Optional.class)
                .setIncludeAsProperty(JsonInclude.Value
                        .construct(JsonInclude.Include.NON_NULL, null));
        return objectMapper;
    }

    @Test
    public void testRoundTrip() throws Exception {
        String originalJson = "{\"one\":null,\"two\":2,\"three\":null}";
        ObjectMapper mapper = initObjectMapper();

        MyBean bean = mapper.readValue(originalJson, MyBean.class);
        String resultingJson = mapper.writeValueAsString(bean);
        // SUCCESS: no "four:null" field is being added
        Assert.assertEquals(originalJson, resultingJson);
    }
}

jackson-databind 版本2.9.0

之前的解决方法

一个有效的解决方案是覆盖JacksonAnnotationIntrospector并将其设置在ObjectMapper上。

只需包含自定义的introspector类并将initObjectMapper()方法更改为以下内容,并且给定的测试将成功:

public static class OptionalAwareAnnotationIntrospector
        extends JacksonAnnotationIntrospector {
    @Override
    public JsonInclude.Value findPropertyInclusion(Annotated a) {
        if (Optional.class.equals(a.getRawType())) {
            return JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL);
        }
        return super.findPropertyInclusion(a);
    }
}

private ObjectMapper initObjectMapper() {
    return new ObjectMapper()
            .registerModule(new Jdk8Module())
            .setSerialisationInclusion(JsonInclude.Include.ALWAYS)
            .setAnnotationIntrospector(new OptionalAwareAnnotationIntrospector());
}

答案 1 :(得分:1)

到目前为止我能够进行调查,问题不在于混合,而在于@JsonInclude的语义。对于类型的包含附件的含义有两种可能的解释:

  1. 对所有类型的值使用该包含(除非被每个属性注释覆盖)
  2. 对所有类型定义的POJO属性使用该包含(除非被每个属性注释覆盖)
  3. 这里的期望似乎是(1),但杰克逊实际上是出于历史原因(2)。这允许容易地违反给定POJO的所有属性,例如:

    @JsonInclude(Include.NON_NULL)
    public class Stuff {
        public String name;
        public Address address;
    }
    

    但不是,例如

    @JsonInclude(Include.NON_NULL)
    public class Address {
    }
    

    要过滤掉所有null - 值Address - 属性。