How to deserialise anonymous array of mixed types with Jackson

Tags: , , , ,



In my Java program, I am trying to parse data that I get from Strava.com‘s API. One of the JSON payloads, I receive from there looks as follows:

[
  {"type": "altitude","data": [519.1,519.3,519.3,519.4,519.5],"series_type": "distance","original_size": 5,"resolution": "high"},
  {"type": "latlng","data": [[46.01234,6.01234],[46.11234,6.11234],[46.21234,6.21234],[46.31234,6.31234],[46.41234,6.41234]],"series_type": "distance","original_size": 5,"resolution": "high"},
  {"type": "velocity_smooth","data": [0.0,0.0,0.0,5.5,5.2],"series_type": "distance","original_size": 5,"resolution": "high"},
  {"type": "distance","data": [0.0,8.6,11.8,16.6,20.8],"series_type": "distance","original_size": 5,"resolution": "high"},
  {"type": "time","data": [0,1,2,3,4],"series_type": "distance","original_size": 5,"resolution": "high"}
]

Basically, four of these entries (altitude, velocity_smooth, distance and time) have the same structure (their data field is an array of doubles (or ints that can be parsed as doubles)), but the second entry (latlng) has a slighlty different structure for the data field (it is a an array of arrays of double).

I am familiar with the Jackson library to convert between JSON and POJOs if all the content is named, but do not see how I can model the above data structure to deserialise it.

Let’s say that instead of the data above, it looked as follows:

{
  "altitude": {"data": [519.1,519.3,519.3,519.4,519.5],"series_type": "distance","original_size": 5,"resolution": "high"},
  "latlng":  {"data": [[46.01234,6.01234],[46.11234,6.11234],[46.21234,6.21234],[46.31234,6.31234],[46.41234,6.41234]],"series_type": "distance","original_size": 5,"resolution": "high"},
  "velocity_smooth": {"data": [0.0,0.0,0.0,5.5,5.2],"series_type": "distance","original_size": 5,"resolution": "high"},
  "distance":  {"data": [0.0,8.6,11.8,16.6,20.8],"series_type": "distance","original_size": 5,"resolution": "high"},
  "time": {"data": [0,1,2,3,4],"series_type": "distance","original_size": 5,"resolution": "high"}
}

Then I could define the following three classes

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Value;
import java.util.List;

@Getter
@NoArgsConstructor
public class Holder {
  DoubleData altitude;
  CoordinateData latlng;
  @JsonProperty("velocity_smooth") DoubleData velocitySmooth;
  DoubleData distance;
  DoubleData time;
}

@Getter
@NoArgsConstructor
public class DoubleData {
  List<Double> data;
  @JsonProperty("series_type")   String seriesType;
  @JsonProperty("original_size") Integer originalSize;
  String resolution;
}

@Getter
@NoArgsConstructor
public class CoordinateData {
  List<List<Double>> data;
  @JsonProperty("series_type")   String seriesType;
  @JsonProperty("original_size") Integer originalSize;
  String resolution;
}

And then use

objectMapper.readValue(jsonString, Holder.class);

to read in that object. However, as the data received from Strava is an array instead of an object, I am failing. I have read Baeldung’s article on how to unmarshal to collections/arrays but that assumes that all classes in the array/collection are the same.

I though about defining an interface which would be extended by the two classes that could be found in the array and then use that mechanism:

public interface Data {
}

@Getter
@NoArgsConstructor
public class DoubleData implements Data {
  String type;
  List<Double> data;
  @JsonProperty("series_type")   String seriesType;
  @JsonProperty("original_size") Integer originalSize;
  String resolution;
}

@Getter
@NoArgsConstructor
public class CoordinateData implements Data {
  String type;
  List<List<Double>> data;
  @JsonProperty("series_type")   String seriesType;
  @JsonProperty("original_size") Integer originalSize;
  String resolution;
}

Data[] array = objectMapper.readValue(jsonString, Data[].class);

But that doesn’t work, as I would need to find some way to let it find out when to use a DoubleData class and when to use a CoordinateData class.

I am sure, I am not the first person trying to use Strava data in Java. Can this be done?

Answer

If possible, you should definitely use their’s client. Strava API v3 shows many examples how to use this API together with theirs model.

If you want to implement your own model you should consider inheritance and com.fasterxml.jackson.annotation.JsonTypeInfo, com.fasterxml.jackson.annotation.JsonSubTypes annotations. Also, JSON Object with type latlng contains list of objects which are represented in JSON in form of array. We can handle this using com.fasterxml.jackson.annotation.JsonFormat annotation. All together gives:

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.StringJoiner;

public class StravaApp {
    public static void main(String[] args) throws IOException {
        File jsonFile = new File("./resource/test.json").getAbsoluteFile();

        ObjectMapper mapper = new ObjectMapper();
        mapper.readValue(jsonFile, new TypeReference<List<Data>>() {}).forEach(System.out::println);
    }
}

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.EXISTING_PROPERTY,
        visible = true,
        property = "type")
@JsonSubTypes({
        @JsonSubTypes.Type(name = "altitude", value = DoubleData.class),
        @JsonSubTypes.Type(name = "latlng", value = CoordinateData.class),
        @JsonSubTypes.Type(name = "velocity_smooth", value = DoubleData.class),
        @JsonSubTypes.Type(name = "distance", value = DoubleData.class),
        @JsonSubTypes.Type(name = "time", value = DoubleData.class)
})
abstract class Data<T> {

    private String type;

    @JsonProperty("series_type")
    private String seriesType;

    @JsonProperty("original_size")
    private Integer originalSize;

    private String resolution;

    private List<T> data;

    // getters, setters, toString
}


class DoubleData extends Data<Double> {

}

class CoordinateData extends Data<Coordinates> {

}

@JsonFormat(shape = JsonFormat.Shape.ARRAY)
class Coordinates {
    private double lat;
    private double lng;

        // getters, setters, toString
}

Above code prints:

Data[type='altitude', seriesType='distance', originalSize=5, resolution='high', data=[519.1, 519.3, 519.3, 519.4, 519.5]]
Data[type='latlng', seriesType='distance', originalSize=5, resolution='high', data=[Coordinates[lat=46.01234, lng=6.01234], Coordinates[lat=46.11234, lng=6.11234], Coordinates[lat=46.21234, lng=6.21234], Coordinates[lat=46.31234, lng=6.31234], Coordinates[lat=46.41234, lng=6.41234]]]
Data[type='velocity_smooth', seriesType='distance', originalSize=5, resolution='high', data=[0.0, 0.0, 0.0, 5.5, 5.2]]
Data[type='distance', seriesType='distance', originalSize=5, resolution='high', data=[0.0, 8.6, 11.8, 16.6, 20.8]]
Data[type='time', seriesType='distance', originalSize=5, resolution='high', data=[0.0, 1.0, 2.0, 3.0, 4.0]]

You should also take a look on Google Dev Group and consult this solution.



Source: stackoverflow