6

I'm consuming a web service using Spring's RestTemplate and deserializing with Jackson.

In my JSON response from the server, one of the fields can be either an object or a list. meaning it can be either "result": [{}] or "result": {}.

Is there a way to handle this kind of things by annotations on the type I'm deserializing to ? define the member as an array[] or List<> and insert a single object in case of the second example ? Can I write a new HttpMessageConverter that will handle it ?

Nati
  • 1,034
  • 5
  • 19
  • 46

2 Answers2

7

Since you are using Jackson I think what you need is JsonDeserializer class (javadoc).

You can implement it like this:

public class ListOrObjectGenericJsonDeserializer<T> extends JsonDeserializer<List<T>> {

    private final Class<T> cls;

    public ListOrObjectGenericJsonDeserializer() {
        final ParameterizedType type = (ParameterizedType) this.getClass().getGenericSuperclass();
        this.cls = (Class<T>) type.getActualTypeArguments()[0];
    }

    @Override
    public List<T> deserialize(final JsonParser p, final DeserializationContext ctxt) throws IOException, JsonProcessingException {
        final ObjectCodec objectCodec = p.getCodec();
        final JsonNode listOrObjectNode = objectCodec.readTree(p);
        final List<T> result = new ArrayList<T>();
        if (listOrObjectNode.isArray()) {
            for (JsonNode node : listOrObjectNode) {
                result.add(objectCodec.treeToValue(node, cls));
            }
        } else {
            result.add(objectCodec.treeToValue(listOrObjectNode, cls));
        }
        return result;
    }
}

...

public class ListOrObjectResultItemJsonDeserializer extends ListOrObjectGenericJsonDeserializer<ResultItem> {}

Next you need to annotate your POJO field. Let's say you have classes like Result and ResultItem:

public class Result {

    // here you add your custom deserializer so jackson will be able to use it
    @JsonDeserialize(using = ListOrObjectResultItemJsonDeserializer.class)
    private List<ResultItem> result;

    public void setResult(final List<ResultItem> result) {
    this.result = result;
    }

    public List<ResultItem> getResult() {
        return result;
    }
}

...

public class ResultItem {

    private String value;

    public String getValue() {
        return value;
    }

    public void setValue(final String value) {
        this.value = value;
    }
}

Now you can check your deserializer:

// list of values
final String json1 = "{\"result\": [{\"value\": \"test\"}]}";
final Result result1 = new ObjectMapper().readValue(json1, Result.class);
// one value
final String json2 = "{\"result\": {\"value\": \"test\"}}";
final Result result2 = new ObjectMapper().readValue(json2, Result.class); 

result1 and result2 contain the same value.

Kuvaldis
  • 348
  • 1
  • 9
  • Cool. can it be generic ? say something like `public List deserialize(final JsonParser p, final DeserializationContext ctxt) throws IOException, JsonProcessingException {` ? – Nati Feb 11 '16 at 15:07
  • I changed it to generic. But you have to extend the generic base class and specify field type anyway. – Kuvaldis Feb 11 '16 at 15:23
  • result.add(objectCodec.treeToValue(node, cls)); // this is trying to parse the items of the list, also as lists. Is this correct? – Radu Simionescu Jun 23 '17 at 15:16
5

You can achieve what you want with a configuration flag in Jackson's ObjectMapper:

ObjectMapper mapper = Jackson2ObjectMapperBuilder.json()
    .featuresToEnable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
    .build();

Just set this ObjectMapper instance to your RestTemplate as explained in this answer, and in the class you are deserializing to, always use a collection, i.e. a List:

public class Response {

    private List<Result> result;

    // getter and setter
}
Community
  • 1
  • 1
fps
  • 33,623
  • 8
  • 55
  • 110
  • That sounds great, can I set this flag on specific fields ? I don't want to apply it on every object. – Nati Feb 11 '16 at 14:58
  • 1
    @Nati I don't believe you can apply it to specific fields. However, if you have a single field in your JSON and you know for sure that it will never be a collection, then you can have a single field attribute in your java `Response` class (i.e. not a `List`). – fps Feb 11 '16 at 15:14
  • It worked ! the only thing you need to change is `ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().build();` instead of `new`ing the object, and then I'll accept the answer – Nati Feb 11 '16 at 18:49
  • @Nati Done. Also changed the way the flag was being set. – fps Feb 11 '16 at 19:06