Mark Mark - 3 months ago 21
Java Question

Deserialize JSON array to Map using Jackson

I have JSON structured like:

{
"id" : "123",
"name" : [ {
"stuff" : [ {
"id" : "234",
"name" : "Bob"
}, {
"id" : "345",
"name" : "Sally"
} ]
} ]
}


That I want to map to the following data structure:

MyInterface1

@Value.Immutable
@JsonSerialize(as = ImmutableMyInterface1.class)
@JsonDeserialize(as = ImmutableMyInterface1.class)
public interface MyInterface1 {
String id();
@JsonDeserialize(using = MyInterface1Deserializer.class)
List<MyInterface2> name();
}


MyInterface2

@Value.Immutable
@JsonSerialize(as = ImmutableMyInterface2.class)
@JsonDeserialize(as = ImmutableMyInterface2.class)
public interface MyInterface2 {
@JsonDeserialize(using = StuffDeserializer.class)
Map<String, MyInterface3> stuff();
}


MyInterface3

@Value.Immutable
@JsonSerialize(as = ImmutableMyInterface3.class)
@JsonDeserialize(as = ImmutableMyInterface3.class)
public interface MyInterface3 {
String id();
String name();
}


I'm using an ObjectMapper with
readValue(stringWithJson,MyInterface1.class)
to map this JSON to MyInterface1, which should continue down the chain to MyInterface3. This setup was working when I was using a List in MyInterface2, i.e.
List<MyInterface3> name();


However, I want this to be a map instead of a list, ideally with "id" from the inner JSON as the key. This would allow me to get values with the following syntax:
MyInterface1.get(0).MyInterface2.get("id1").name();


The problem is that when attempting to create a custom StuffDeserializer.class, I'm getting the error:
Can not deserialize instance of com.foo.ImmutableMyInterface2$Json out of START_ARRAY token


when trying to do:

public Map<String, MyInterface3> deserialize(JsonParser jsonParser, DeserializationContext ctxt)
throws IOException {

MyInterface2 foo = Unmarshaller.OBJECT_MAPPER.readValue(jsonParser, MyInterface2.class); // error here
...


I think this is because Jackson is expecting "stuff" to be a List 'cause of the JSON array. What's the best way to deserialize this JSON to a map that uses values from the inner JSON as a key?

Answer

I would create a custom JsonDeserializer to map id and name into a map:

public class StringHashMapValueDeserializer extends JsonDeserializer<HashMap<String, String>>{

    @Override
    public HashMap<String, String> deserialize(JsonParser parser, DeserializationContext ctxt)
            throws IOException, JsonProcessingException {
        HashMap<String, String> ret = new HashMap<String, String>();

        ObjectCodec codec = parser.getCodec();
        TreeNode node = codec.readTree(parser);

        if (node.isArray()){
            for (JsonNode n : (ArrayNode)node){
                JsonNode id = n.get("id");
                if (id != null){
                    JsonNode name = n.get("name");
                    ret.put(id.asText(), name.asText());
                }
            }
        }
        return ret;
    }
}

And then I would create simple beans with annotating stuff property with the deserializer:

@Getter
@Setter
public class Name {

    @JsonDeserialize(using = StringHashMapValueDeserializer.class)
    Map<String, String> stuff;

    @Override
    public String toString() {
        return "Name [stuff=" + stuff + "]";
    }
}

Outer type:

@Getter
@Setter
public class OuterType {

    String id;
    List<Name> name;

    @Override
    public String toString() {
        return "OuterType [id=" + id + ", stuff=" + name + "]";
    }
}

Deserialization:

ObjectMapper mapper = new ObjectMapper();

OuterType response;
response = mapper.readValue(json, OuterType.class);

System.out.println(response);
System.out.println(response.getName().get(0).getStuff().get("234"));

console output:

OuterType [id=123, stuff=[Name [stuff={234=Bob, 345=Sally}]]]
Bob

Hope it helps.

Comments