bgraves
bgraves

Reputation: 839

Jackson: Add dynamic field to Map serialization (sth like @JsonAppend)

Something like a simplified Version of @JsonAppend

public class Bean {

    @JsonAppend(key = [...], value = [...])
    public Map<?, ?> map = new HashMap<>();
}

would be great - any simple way to achieve this?

I've read lot's of SO entries, eg.

but found nothing matching my needs.

The reason for my request is that it is indistinguishable whether some given JSON originated from Map or POJO serialization. If this is necessary (in rare cases), adding a magic extra field to the map would be a simple way to achieve this.

Upvotes: 3

Views: 1868

Answers (1)

LppEdd
LppEdd

Reputation: 21152

Great question! Yes, this is (somehow) possible. The following exposed methodology maintains the standard serialization behavior, while adding on top of it annotation-defined key-value pairs.


Create a custom annotation. I'll call it MapAppender

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MapAppender {
    String[] keys();
    String[] values();
}

As you can see, we define key-value arrays, which will match by index.
We are forced using String fields instead of the more generic Object, but that's per annotation design.

Create a custom JsonSerializer<Map>. I'll call it MapAppenderSerializer

public class MapAppenderSerializer
        extends StdSerializer<Map>
        implements ContextualSerializer {
    private static final long serialVersionUID = 1L;

    private final String[] keys;
    private final String[] values;

    // No-arg constructor required for Jackson
    MapAppenderSerializer() {
        super(Map.class);
        keys = new String[0];
        values = new String[0];
    }

    MapAppenderSerializer(
            final String[] keys,
            final String[] values) {
        super(Map.class);
        this.keys = keys;
        this.values = values;
    }

    @Override
    public void serialize(
            final Map value,
            final JsonGenerator jsonGenerator,
            final SerializerProvider serializerProvider) throws IOException {
        // Create a copy Map to avoid touching the original one
        final Map hashMap = new HashMap<>(value);

        // Add the annotation-specified key-value pairs
        for (int i = 0; i < keys.length; i++) {
            hashMap.put(keys[i], values[i]);
        }

        // Serialize the new Map
        serializerProvider.defaultSerializeValue(hashMap, jsonGenerator);
    }

    @Override
    public JsonSerializer<?> createContextual(
            final SerializerProvider serializerProvider,
            final BeanProperty property) {
        MapAppender annotation = null;

        if (property != null) {
            annotation = property.getAnnotation(MapAppender.class);
        }

        if (annotation != null) {
            return new MapAppenderSerializer(annotation.keys(), annotation.values());
        }

        throw new UnsupportedOperationException("...");
    }
}

Now, using your Bean class example, annotate the Map field with @MapAppender and define a custom serializer using @JsonSerialize

public class Bean {
    public String simpleField;

    @MapAppender(keys = {"test1", "test2"}, values = {"value1", "value2"})
    @JsonSerialize(using = MapAppenderSerializer.class)
    public Map<Object, Object> simpleMap = new HashMap<>();
}

That's it. Serializing an instance of Bean

final ObjectMapper objectMapper = new ObjectMapper();
final String string = objectMapper.writeValueAsString(new Bean());

results in

{"simpleField":null,"simpleMap":{"test2":"value2","test1":"value1"}}

Another example, having the Map populated with values prior to serialization

final ObjectMapper objectMapper = new ObjectMapper();
final Bean value = new Bean();
value.simpleMap.put("myKey", "myValue");

final String string = objectMapper.writeValueAsString(value);

results in

{"simpleField":null,"simpleMap":{"test1":"value1","test2":"value2","myKey":"myValue"}}

Upvotes: 5

Related Questions