I am having issues when trying to deserializing the following class:
public class MetricValuesDto { private Map<MetricType, MetricValueDto<?>> metricValues; public MetricValuesDto() { } public MetricValuesDto(Map<MetricType, MetricValueDto<?>> metricValues) { this.metricValues = metricValues; } public Map<MetricType, MetricValueDto<?>> getMetricValues() { return metricValues; } public void setMetricValues(Map<MetricType, MetricValueDto<?>> metricValues) { this.metricValues = metricValues; } }
My generic abstract class:
public abstract class MetricValueDto<T> { private T value; private MetricTrend trend; public MetricValueDto(T value, MetricTrend trend) { this.value = value; this.trend = trend; } public T getValue() { return value; } public void setValue(T value) { this.value = value; } public MetricTrend getTrend() { return trend; } public void setTrend(MetricTrend trend) { this.trend = trend; } }
I have two concrete classes which implement MetricValueDto
:
IntMetricValueDto:
public class IntMetricValueDto extends MetricValueDto<Integer> { public IntMetricValueDto(Integer value, MetricTrend trend) { super(value, trend); } }
FloatMetricValueDto:
public class FloatMetricValueDto extends MetricValueDto<Float> { public FloatMetricValueDto(Float value, MetricTrend trend) { super(value, trend); } }
Any idea of what’s the correct strategy to deserialize MetricValueDto so I can parse it through ObjectMapper or an RestTemplate? Whenever I run:
restTemplate.exchange("myEndpoint", HttpMethod.GET, entity, DataCollectionEventDto.class);
I get
Caused by: com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of com.resson.dto.MetricValueDto: abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
DataCollectionEventDto:
public class DataCollectionEventDto { private List<MapLayerDto> mapLayers; @JsonUnwrapped private MetricValuesDto metricValues; public List<MapLayerDto> getMapLayers() { return mapLayers; } public void setMapLayers(List<MapLayerDto> mapLayers) { this.mapLayers = mapLayers; } public MetricValuesDto getMetricValues() { return metricValues; } public void setMetricValues(MetricValuesDto metricValues) { this.metricValues = metricValues; } @Override public String toString() { return ToStringBuilder.reflectionToString(this); } }
I have basically tried everything on web and I could not make it work; any suggestion would be helpful.
Advertisement
Answer
While JsonTypeInfo
works, and adds implementation-specific detail to the response, which later might add confusion to the API client.
I ended up implementing a custom StdDeserializer:
public class MetricValueDtoDeserializer<T> extends StdDeserializer<MetricValueDto<T>> { private static final long serialVersionUID = 1L; public MetricValueDtoDeserializer() { this(null); } public MetricValueDtoDeserializer(Class<?> vc) { super(vc); } private ObjectMapper mapper; @Override public MetricValueDto<T> deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException, JsonProcessingException { String metricType = jsonParser.getCurrentName(); mapper = (ObjectMapper) jsonParser.getCodec(); ObjectNode objectNode = (ObjectNode) mapper.readTree(jsonParser); Iterator<Entry<String, JsonNode>> elementsIterator = objectNode.fields(); Number number = null; while (elementsIterator.hasNext()) { Entry<String, JsonNode> element = elementsIterator.next(); String key = element.getKey(); if (key.equals("value")) { number = parseValue(element, metricType); } if (key.equals("trend")) { MetricTrend metricTrend = parseTrend(element); return (produceMetricValueDto(number, metricTrend)); } } throw new IOException(); } @SuppressWarnings("unchecked") private MetricValueDto<T> produceMetricValueDto(Number number, MetricTrend metricTrend) throws IOException { if (number instanceof Integer) { return (MetricValueDto<T>) new IntMetricValueDto((Integer) number, metricTrend); } else if (number instanceof Float) { return (MetricValueDto<T>) new FloatMetricValueDto((Float) number, metricTrend); } throw new IOException(); } private MetricTrend parseTrend(Entry<String, JsonNode> element) throws JsonProcessingException { String trend = mapper.treeToValue(element.getValue(), String.class); if (trend == null) { return null; } else { return MetricTrend.valueOf(trend); } } private Number parseValue(Entry<String, JsonNode> element, String metricType) throws IOException { if (metricType.equals(MetricType.CANOPY_COVERAGE.toValue()) || metricType.equals(MetricType.PLANT_SIZE.toValue())) { return mapper.treeToValue(element.getValue(), Float.class); } else if (metricType.equals(MetricType.INSECT_COUNT.toValue()) || metricType.equals(MetricType.PLANT_COUNT.toValue())) { return mapper.treeToValue(element.getValue(), Integer.class); } throw new IOException(); } }
The code ended up to being more complex than JsonTypeInfo
, but the API client is unaware of implementation-specific details.