opendatalab-de / geojson-jackson

GeoJson POJOs for Jackson - serialize and deserialize objects with ease
http://blog.opendatalab.de
Apache License 2.0
263 stars 94 forks source link

Turning on the SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED and DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY of ObjectMapper breaks LngLatAlt deserialization #68

Open Strongbeard opened 2 months ago

Strongbeard commented 2 months ago

Software Versions:

Executive Summary

The LngLatAltSerializer and LngLatAltDeserializer cannot properly handle the SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED and DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY features being turned on in an ObjectMapper.

Details

Running the following code throws the below redacted exception. Substituting the Polygon with a similar MultiPolygon or MultiLineString should produce a similar exception.

ObjectMapper mapper = new ObjectMapper()
    .enable(SerializationFeature.INDENT_OUTPUT)
    .enable(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED)
    .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
Polygon polygon = new Polygon(List.of(
    new LngLatAlt(0.0, 0.0),
    new LngLatAlt(1.0, 0.0),
    new LngLatAlt(0.0, 1.0),
    new LngLatAlt(0.0, 0.0)
));
String str = mapper.writeValueAsString(polygon);
mapper.readValue(str, Polygon.class);
com.fasterxml.jackson.databind.JsonMappingException: Cannot deserialize instance of `org.geojson.LngLatAlt` out of VALUE_NUMBER_FLOAT token
 at [Source: (String)"{
  "type" : "Polygon",
  "coordinates" : [ [ 0.0, 0.0 ], [ 1.0, 0.0 ], [ 0.0, 1.0 ], [ 0.0, 0.0 ] ]
}"; line: 3, column: 23] (through reference chain: org.geojson.Polygon["coordinates"]-]java.util.ArrayList[0]-]java.util.ArrayList[0])
  at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:269)
  at com.fasterxml.jackson.databind.DeserializationContext.mappingException(DeserializationContext.java:2201)
  at com.fasterxml.jackson.databind.DeserializationContext.mappingException(DeserializationContext.java:2193)
  at org.geojson.jackson.LngLatAltDeserializer.deserialize(LngLatAltDeserializer.java:20)
  at org.geojson.jackson.LngLatAltDeserializer.deserialize(LngLatAltDeserializer.java:13)
  at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer._deserializeFromArray(CollectionDeserializer.java:359)
  at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:244)
  at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:28)
  at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer._deserializeFromArray(CollectionDeserializer.java:359)
  at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:244)
  at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:28)
  at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129)
  at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:314)
  at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:215)
  at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:187)
  at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:170)
  at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:136)
  at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:1306)
  at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:74)
  at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:323)
  at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4825)
  at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3772)
  at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3740)
  at ██████████(██████████.java:██████████)
  at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
  at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

I believe the issue stems from the following two json Strings that are produced and then interpreted by mapper.readValue(str, Polygon.class) when SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED is turned on or off, respectively. The former throws the exception while the latter does not, due to expecting the 3rd set of array brackets. When the feature is turned on, the outermost brackets are not written since they are "unwrapped" by the feature. Thus, in the former scenario, jackson "opens" both arrays since it knows from the type that there are 2 outer list/array types and tries to pass the 1st DoubleNode to the LngLatAltDeserializer, which expects the [ token instead.

{
  "type" : "Polygon",
  "coordinates" : [ [ 0.0, 0.0 ], [ 1.0, 0.0 ], [ 0.0, 1.0 ], [ 0.0, 0.0 ] ]
}
{
  "type" : "Polygon",
  "coordinates" : [ [ [ 0.0, 0.0 ], [ 1.0, 0.0 ], [ 0.0, 1.0 ], [ 0.0, 0.0 ] ] ]
}

Potential Solutions

  1. Use annotations to turn off the SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED and DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY features for only the geojson classes so that said features can still be turned on for the surrounding json document without breaking serialization/deserialization of the geojson. This might be the prefered solution since RFC7946 does not yet appear to have any examples of "unwrapped single element arrays".
  2. Perhaps there is a way to indicate to the jackson library that LngLatAlt objects should be treated like an array when serializing/deserializing them so it properly generates the 3rd wrapping ArrayNode even when the above features are turned on? Maybe either making LngLatAlt extend List or having the LngLatAltDeserializer advertise that it produces an array by overriding the hangleType() function to return List.class or logicalType() to return LogicalType.Array?
Strongbeard commented 2 months ago

For anyone waiting for this to be resolved, you can work-around the issue by adding the following mixin to your ObjectMapper:

import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;

public abstract class GeometryMixin<T> {
    @JsonFormat(without = JsonFormat.Feature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED)
    protected List<T> coordinates;
}
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.geojson.Geometry;
// import <path-to-mixin>.GeometryMixin;

ObjectMapper mapper = new ObjectMapper()
    .enable(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED)
    .addMixIn(Geometry.class, GeometryMixin.class);