Jackson - combine @JsonValue and @JsonSerialize

2019-05-27 04:34发布

问题:

I am trying a combination of @JsonValue and @JsonSerialize. Let's start with my current container class:

public class Container {
    private final Map<SomeKey, Object> data;

    @JsonValue
    @JsonSerialize(keyUsing = SomeKeySerializer.class)
    public Map<SomeKey, Object> data() {
        return data;
    }
}

In this case, the custom serializer SomeKeySerializer is not used.

If I change the container as following, the serializer is called:

public class Container {
    @JsonSerialize(keyUsing = SomeKeySerializer.class)
    private final Map<SomeKey, Object> data;
}

However, this is not what I want, as this introduces another 'data' level in the output JSON.

Is it possible to combine @JsonValue and @JsonSerialize in some way?

I could always write another custom serializer for Container, which more or less does the same as the functionality behind @JsonValue. This would be more or less a hack, in my opinion.

Jackson version: 2.6.2

回答1:

This combination seems to do what you want: make a Converter to extract the Map from the Container, and add @JsonValue to SomeKey itself to serialize it:

@JsonSerialize(converter = ContainerToMap.class)
public class ContainerWithFieldData {
    private final Map<SomeKey, Object> data;

    public ContainerWithFieldData(Map<SomeKey, Object> data) {
        this.data = data;
    }
}

public static final class SomeKey {
    public final String key;

    public SomeKey(String key) {
        this.key = key;
    }

    @JsonValue
    public String toJsonValue() {
        return "key:" + key;
    }

    @Override
    public String toString() {
        return "SomeKey:" + key;
    }
}

public static final class ContainerToMap extends StdConverter<ContainerWithFieldData, Map<SomeKey, Object>> {
    @Override
    public Map<SomeKey, Object> convert(ContainerWithFieldData value) {
        return value.data;
    }
}

@Test
public void serialize_container_with_custom_keys_in_field_map() throws Exception {
    ObjectMapper mapper = new ObjectMapper();
    assertThat(
            mapper.writeValueAsString(new ContainerWithFieldData(ImmutableMap.of(new SomeKey("key1"), "value1"))),
            equivalentTo("{ 'key:key1' : 'value1' }"));
}

I simply can't get annotating an accessor method of Container to DTRT at all easily, not in combination with @JsonValue. Given that @JsonValue on the container is basically designating a converter anyway (that is implemented by calling the annotated method), this is effectively what you're after, although not as pleasant as it seems it should be. (tried with Jackson 2.6.2)

(Something I learned from this: key serializers aren't like normal serializers, even though they implement JsonSerializer just the same. They need to call writeFieldName on the JsonGenerator, not writeString, for example. On the deserialization side, the distinction between JsonDeserializer and KeyDeserializer is spelled out, but not on the serialization side. You can make a key serializer from SomeKey with @JsonValue, but not by annotating SomeKey with @JsonSerialize(using=...), which surprised me).



回答2:

Have you tried using @JsonSerialize(using = SomeKeySerializer.class) instead of keyUsing?

Doc for using() says:

Serializer class to use for serializing associated value.

...while for keyUsing you get:

Serializer class to use for serializing Map keys of annotated property

Tested it out myself and it works...

public class Demo {

  public static class Container {

    private final Map<String, String> data = new HashMap<>();

    @JsonValue
    @JsonSerialize(using = SomeKeySerializer.class)
    public Map<String, String> data() {
      return data;
    }
  }

  public static class SomeKeySerializer extends JsonSerializer<Map> {

    @Override
    public void serialize(Map value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
      jgen.writeStartObject();
      jgen.writeObjectField("aKeyInTheMap", "theValueForThatKey");
      jgen.writeEndObject();
    }
  }

  public static void main(String[] args) throws JsonProcessingException {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
    String s = objectMapper.writeValueAsString(new Container());
    System.out.println(s);
  }
}

This is the output when I'm NOT using com.fasterxml.jackson.annotation.JsonValue

{
  "data" : {
    "aKeyInTheMap" : "theValueForThatKey"
  }
}

And this is the output when I'm using com.fasterxml.jackson.annotation.JsonValue

{
  "aKeyInTheMap" : "theValueForThatKey"
}