4

I'm trying to serialize/deserialize a Map<?, ?> with arbitrary object as keys with Jackson version 2.8. The JSON counterpart should be an array of couples, i.e. given

public class Foo {
    public String foo;
    public Foo(String foo) {
        this.foo = foo;
    }
}

public class Bar {
    public String bar;
    public Bar(String bar) {
        this.bar = bar;
    }
}

then

Map<Foo, Bar> map;
map.put(new Foo("foo1"), new Bar("bar1"));
map.put(new Foo("foo2"), new Bar("bar2"));

should be represented by this JSON

[
    [ { "foo": "foo1" }, { "bar": "bar1" } ],
    [ { "foo": "foo2" }, { "bar": "bar2" } ]
]

So I did the serializer part as

public class MapToArraySerializer extends JsonSerializer<Map<?, ?>> {

    @Override
    public void serialize(Map<?, ?> value, JsonGenerator gen, SerializerProvider serializers)
        throws IOException, JsonProcessingException {
        gen.writeStartArray();
        for (Map.Entry<?, ?> entry : value.entrySet()) {
            gen.writeStartArray();
            gen.writeObject(entry.getKey());
            gen.writeObject(entry.getValue());
            gen.writeEndArray();
        }
        gen.writeEndArray();
    }

}

but I have no idea how to write a JsonDeserializer to do the inverse job. Any suggestions?

Note: I need the [ [ "key1", "value1" ], [ "key2", "value2" ] ] notation to be able to consume that JSON in JavaScript a new Map( ... ) and JSON.stringify(map) would produce that notation too (see https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Map).

To clarify, such a map would be a field of other classes, e.g.

public class Baz {

    @JsonSerialize(using = MapToArraySerializer.class)
    @JsonDeserialize(using = ArrayToMapDeserializer.class, keyAs = Foo.class, contentAs = Bar.class)
    Map<Foo, Bar> map;

}

and ArrayToMapDeserializer extends JsonDeserializer<Map<?, ?>> is where I'm asking for help.

Giovanni Lovato
  • 1,882
  • 2
  • 21
  • 41
  • I think you are over complicating it. Your JSON is not a map. Why are you using a `Map` to represent it? Do `Foo` and `Bar` have any attribute that make them different? – cassiomolin Sep 15 '16 at 12:25
  • In real use case scenarios `Foo` and `Bar` are complex objects and a map with `Foo` as key-type may be a field of another object. I need to port this Java maps to JavaScript, which has its own `Map` which serializes in that form (see the link to MDN in the question). – Giovanni Lovato Sep 15 '16 at 12:55
  • Yes, they could be totally different objects (any Java object). I've updated the question to make that clear! – Giovanni Lovato Sep 15 '16 at 13:00
  • Could they implement the same interface or extend the same class? – cassiomolin Sep 15 '16 at 13:02
  • Unfortunately no, I'm looking for a generalized way to (de)serialize Java maps with this notation, e.g. any Java class should work as key-type. – Giovanni Lovato Sep 15 '16 at 13:04
  • If they can be any Java object, how do you expect to know which type you are going to instantiate when deserializing the JSON? – cassiomolin Sep 15 '16 at 13:07
  • Telling Jackson with `@JsonDeserialize` annotation `keyAs` and `contentAs` properties (see updated question). – Giovanni Lovato Sep 15 '16 at 13:08

2 Answers2

9

I came up with this solution:

public class ArrayToMapDeserializer extends JsonDeserializer<SortedMap<Object, Object>>
    implements ContextualDeserializer {

    private Class<?> keyAs;

    private Class<?> contentAs;

    @Override
    public Map<Object, Object> deserialize(JsonParser p, DeserializationContext ctxt)
        throws IOException, JsonProcessingException {
        return this.deserialize(p, ctxt, new HashMap<>());
    }

    @Override
    public Map<Object, Object> deserialize(JsonParser p, DeserializationContext ctxt,
        Map<Object, Object> intoValue) throws IOException, JsonProcessingException {
        JsonNode node = p.readValueAsTree();
        ObjectCodec codec = p.getCodec();
        if (node.isArray()) {
            node.forEach(entry -> {
                try {
                    JsonNode keyNode = entry.get(0);
                    JsonNode valueNode = entry.get(1);
                    intoValue.put(keyNode.traverse(codec).readValueAs(this.keyAs),
                        valueNode.traverse(codec).readValueAs(this.contentAs));
                } catch (NullPointerException | IOException e) {
                    // skip entry
                }
            });
        }
        return intoValue;
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
        throws JsonMappingException {
        JsonDeserialize jsonDeserialize = property.getAnnotation(JsonDeserialize.class);
        this.keyAs = jsonDeserialize.keyAs();
        this.contentAs = jsonDeserialize.contentAs();
        return this;
    }

}

which can be used like this:

public class Baz {

    @JsonSerialize(using = MapToArraySerializer.class)
    @JsonDeserialize(using = ArrayToMapDeserializer.class,
        keyAs = Foo.class, contentAs = Bar.class)
    Map<Foo, Bar> map;

}
Giovanni Lovato
  • 1,882
  • 2
  • 21
  • 41
0

Here is the deserialize:

@Override
public Map<?, ?> deserialize(JsonParser p, DeserializationContext ctxt)
        throws IOException, JsonProcessingException {
    Map map = new LinkedHashMap();

    ObjectCodec oc = p.getCodec();
    JsonNode anode = oc.readTree(p);

    for (int i = 0; i < anode.size(); i++) {
        JsonNode node = anode.get(i);
        map.put(node.get(0), node.get(1));
    }

    return map;
}

I added a few test cases, with a new Oson implementation, to the original solution, in which I used oson to do the conversion, but with a different convension: map to json: {key1: value1, key2: value2, ...}, so the json output becomes:

{
  {
    "foo": "foo1"
  }: {
    "bar": "bar1"
  },
  {
    "foo": "foo2"
  }: {
    "bar": "bar2"
  }
}

You can check out the source code!

David He
  • 54
  • 3
  • 1
    I asked for Jackson and a specific output, you provide a different tool and a different output. Please check this out: http://stackoverflow.com/help/how-to-answer – Giovanni Lovato Sep 15 '16 at 15:39
  • without proper configuration, jackson got this: {"ca.oson.json.listarraymap.Foo@5f5a92bb":{"bar":"bar1"},"ca.oson.json.listarraymap.Foo@6fdb1f78":{"bar":"bar2"}} – David He Sep 15 '16 at 18:18
  • ArrayToMapDeserializer should not too difficult to implement, when i have time in the evening i will try one for you – David He Sep 15 '16 at 18:25
  • Thank you for your effort but once again your answer does not respect the specifications needed in the question. Please see the other answer for a working solution! – Giovanni Lovato Sep 20 '16 at 16:51