After much trial and error I've been able to implement a workaround for
including the type on NaN and infinity Doubles during the serialization of
the desired Map objects, without getting in the general code path for
Double serialization.
It's a little hacky (in particular I'm not retrieving the TypeResolver
completely dynamically - advice welcome there) but it might keep me going
until the Jackson lib supports this, and it should help in case anyone has
the same use case.
Please let me know if you have better ideas!
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.jsontype.impl.ClassNameIdResolver;
import com.fasterxml.jackson.databind.jsontype.impl.StdTypeResolverBuilder;
import com.fasterxml.jackson.databind.ser.std.MapSerializer;
import com.fasterxml.jackson.databind.ser.std.NumberSerializers;
import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer;
import com.fasterxml.jackson.databind.type.SimpleType;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import static
com.fasterxml.jackson.databind.ser.impl.PropertySerializerMap.emptyForProperties;
public class Repro {
private static ObjectMapper objectMapper = new ObjectMapper();
public static void main(String[] args) {
try {
String beanString = objectMapper.writeValueAsString(new Bean(1L,
Double.NaN));
System.out.println(beanString);
MapHolder beanOut = objectMapper.readValue(beanString,
MapHolder.class);
System.out.println(beanOut.data.get("double").getClass());
beanString = objectMapper.writeValueAsString(new Bean(1L, 1D));
System.out.println(beanString);
beanOut = objectMapper.readValue(beanString, MapHolder.class);
System.out.println(beanOut.data.get("double").getClass());
} catch (IOException e) {
e.printStackTrace();
}
}
public static class Bean {
private Long longValue;
private Double doubleValue;
public Bean(Long longValue, Double doubleValue) {
this.longValue = longValue;
this.doubleValue = doubleValue;
}
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonSerialize(using = CustomMapSerializer.class)
public Map<String, Object> getData() {
Map<String, Object> map = new HashMap<>();
map.put("long", longValue);
map.put("double", doubleValue);
return map;
}
}
public static class MapHolder {
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public Map<String, Object> data;
MapHolder() {
}
}
public static class CustomDoubleSerializer extends JsonSerializer<Object> {
private NumberSerializers.DoubleSerializer doubleSerializer;
private StdScalarSerializer<Object> scalarSerializer;
public CustomDoubleSerializer() {
this.doubleSerializer = new
NumberSerializers.DoubleSerializer(Double.class);
this.scalarSerializer = new
StdScalarSerializer<Object>(Object.class) {
@Override
public void serialize(Object aDouble, JsonGenerator
jsonGenerator, SerializerProvider serializerProvider) throws IOException {
doubleSerializer.serialize(aDouble, jsonGenerator,
serializerProvider);
}
};
}
@Override
public void serialize(Object aDouble, JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException {
doubleSerializer.serialize(aDouble, jsonGenerator,
serializerProvider);
}
@Override
public void serializeWithType(Object aDouble, JsonGenerator
jsonGenerator, SerializerProvider serializerProvider, TypeSerializer
typeSerializer) throws IOException {
if (aDouble instanceof Double && (((Double) aDouble).isInfinite()
|| ((Double) aDouble).isNaN())) {
scalarSerializer.serializeWithType(aDouble, jsonGenerator,
serializerProvider, typeSerializer);
} else {
doubleSerializer.serialize(aDouble, jsonGenerator,
serializerProvider);
}
}
}
public static class CustomMapSerializer extends MapSerializer {
/* Should be used for the initial instantiation, but overwritten by
createContextual */
CustomMapSerializer() {
super(Collections.emptySet(),
SimpleType.constructUnsafe(String.class),
SimpleType.constructUnsafe(Object.class),
false, null, null, null);
}
CustomMapSerializer(MapSerializer src) {
this(src, null, false);
this._dynamicValueSerializers =
emptyForProperties().addSerializer(Double.class, new
CustomDoubleSerializer()).map;
}
CustomMapSerializer(MapSerializer src, Object filterId, boolean
sortKeys) {
super(src, filterId, sortKeys);
this._dynamicValueSerializers =
emptyForProperties().addSerializer(Double.class, new
CustomDoubleSerializer()).map;
}
CustomMapSerializer(MapSerializer src, TypeSerializer vts, Object
suppressableValue, boolean suppressNulls) {
super(src, vts, suppressableValue, suppressNulls);
this._dynamicValueSerializers =
emptyForProperties().addSerializer(Double.class, new
CustomDoubleSerializer()).map;
}
CustomMapSerializer(MapSerializer src, BeanProperty property,
JsonSerializer<?> keySerializer, JsonSerializer<?> valueSerializer, Set<String>
ignoredEntries) {
super(src, property, keySerializer, valueSerializer,
ignoredEntries);
this._dynamicValueSerializers =
emptyForProperties().addSerializer(Double.class, new
CustomDoubleSerializer()).map;
}
@Override
public CustomMapSerializer withResolved(BeanProperty property,
JsonSerializer<?> keySerializer, JsonSerializer<?> valueSerializer, Set<String>
ignored, boolean sortKeys) {
CustomMapSerializer ser = new CustomMapSerializer(this, property,
keySerializer, valueSerializer, ignored);
if (sortKeys != ser._sortKeys) {
ser = new CustomMapSerializer(ser, this._filterId, sortKeys);
}
return ser;
}
@Override
public CustomMapSerializer _withValueTypeSerializer(TypeSerializer vts)
{
if (this._valueTypeSerializer == vts) {
return this;
} else {
return new CustomMapSerializer(this, vts,
this._suppressableValue, this._suppressNulls);
}
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider provider,
BeanProperty property) throws JsonMappingException {
JsonSerializer ser = provider.findValueSerializer(Map.class);
if (ser instanceof MapSerializer) {
MapSerializer mapSer = (MapSerializer) ((MapSerializer)
ser).createContextual(provider, property);
JsonTypeInfo typeInfo =
property.getAnnotation(JsonTypeInfo.class);
// Would be good to find a way to retrieve the TypeSerializer
dynamically...
TypeSerializer typeSer = new
StdTypeResolverBuilder().init(typeInfo.use(), new
ClassNameIdResolver(SimpleType.constructUnsafe(Object.class),
provider.getTypeFactory()))
.inclusion(typeInfo.include()).buildTypeSerializer(provider.getConfig(),
SimpleType.constructUnsafe(Object.class), Collections.emptyList());
CustomMapSerializer customMapSerializer = new
CustomMapSerializer(mapSer);
return customMapSerializer._withValueTypeSerializer(typeSer);
}
return this;
}
}
}
On Sunday, 27 January 2019 10:27:16 UTC, C-B-B wrote:
>
> Many thanks for your response Tatu - looking forward to a fix.
>
> A couple more questions on the approach for the workaround:
>
> - I've implemented the below CustomDoubleSerializer - how bad do you
> think the performance impact would be for our Double serialisations, at a
> high level, between "barely noticeable" and "very bad"?
> - Isn't there a way I can get this to only apply for Map element
> serialisations?
> - Or even better, create a custom MapSerializer that would behave just
> like the defaut MapSerializer, except when it comes to serializing
> Doubles?
> If so what's the easiest way to do so? I'm getting a little lost when
> trying to do that!
>
> Many thanks,
> CBB
>
> PS : apologies for the dark code background, not sure how best to include
> code snippets
>
> import com.fasterxml.jackson.core.JsonGenerator;
> import com.fasterxml.jackson.databind.JsonSerializer;
> import com.fasterxml.jackson.databind.SerializerProvider;
> import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
> import com.fasterxml.jackson.databind.ser.std.NumberSerializers;
> import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer;
>
> import java.io.IOException;
>
> public class CustomDoubleSerializer extends JsonSerializer<Double> {
> private NumberSerializers.DoubleSerializer doubleSerializer;
> private StdScalarSerializer<Double> scalarSerializer;
>
> public CustomDoubleSerializer() {
> this.doubleSerializer = new
> NumberSerializers.DoubleSerializer(Double.class);
> this.scalarSerializer = new StdScalarSerializer<Double>(Double.class)
> {
> @Override
> public void serialize(Double aDouble, JsonGenerator
> jsonGenerator, SerializerProvider serializerProvider) throws IOException {
> doubleSerializer.serialize(aDouble, jsonGenerator,
> serializerProvider);
> }
> };
> }
>
> @Override
> public void serialize(Double aDouble, JsonGenerator jsonGenerator,
> SerializerProvider serializerProvider) throws IOException {
> doubleSerializer.serialize(aDouble, jsonGenerator,
> serializerProvider);
> }
>
> @Override
> public void serializeWithType(Double aDouble, JsonGenerator
> jsonGenerator, SerializerProvider serializerProvider, TypeSerializer
> typeSerializer) throws IOException {
> if (aDouble != null && (aDouble.isInfinite() || aDouble.isNaN())) {
> scalarSerializer.serializeWithType(aDouble, jsonGenerator,
> serializerProvider, typeSerializer);
> } else {
> doubleSerializer.serialize(aDouble, jsonGenerator,
> serializerProvider);
> }
> }
> }
>
>
>
>
>
>
> On Sunday, 27 January 2019 07:22:07 UTC, Tatu Saloranta wrote:
>>
>> On Sat, Jan 26, 2019 at 7:08 PM C-B-B <[email protected]> wrote:
>> >
>> > Hello folks,
>> >
>> > I've been trying to serialise and deserialise a Map<String, Object>
>> whilst preserving the type of the elements, for example a Long should
>> remain a Long and not become an Integer.
>> > This works pretty well using the @JsonTypeInfo annotation on the Map.
>> Jackson is being clever, and only including the type info when it is needed
>> (e.g. it won't include it on an Integer, String, Double etc as they are
>> default deserialisation types anyway), which is nice.
>> > There's however a nasty edge case with Double.NaN,
>> Double.POSITIVE_INFINITY and Double.NEGATIVE_INFINITY: they get serialised
>> without type info, and get deserialised as Strings...
>> >
>> > I've raised this as an issue, but in the meantime I'm looking for a
>> workaround to include the type info on these Doubles (I've checked, and the
>> deserialisation of NaN and infinity values works fine if the type is
>> included).
>> > Ideally, the customised code would only be invoked for that Map object,
>> rather that for all serialisations (I've been able to override the
>> DoubleSerializer altogether on the ObjectMapper, but would rather not make
>> such an invasive change).
>>
>> I agree that this is a bug, and hope to eventually resolve it as per
>> issue filed.
>>
>> But in the meantime I do think that custom deserializer is the way to
>> go; and there's no easy way to change to only occur for Map values
>> (delegation model means that same DoubleDeserializer is used for
>> properties and map values).
>>
>> -+ Tatu +-
>>
>> >
>> > I've tried a few things, including with TypeIdResolver, but no luck so
>> far... Any help would be much appreciated!
>> >
>> > Here's a little repro
>> >
>> > import com.fasterxml.jackson.annotation.JsonTypeInfo;
>> > import com.fasterxml.jackson.databind.ObjectMapper;
>> > import java.io.IOException;
>> > import java.util.HashMap;
>> > import java.util.Map;
>> >
>> > public class Repro {
>> > private static ObjectMapper objectMapper = new ObjectMapper();
>> >
>> > public static void main(String[] args) {
>> > try {
>> > String beanString = objectMapper.writeValueAsString(new
>> Bean(1L, Double.NaN));
>> > MapHolder beanOut = objectMapper.readValue(beanString,
>> MapHolder.class);
>> > System.out.println(beanOut.data.get("double").getClass());
>> > beanString = objectMapper.writeValueAsString(new Bean(1L,
>> 1D));
>> > beanOut = objectMapper.readValue(beanString,
>> MapHolder.class);
>> > System.out.println(beanOut.data.get("double").getClass());
>> > } catch (IOException e) {
>> > e.printStackTrace();
>> > }
>> > }
>> >
>> > public static class Bean {
>> > private Long longValue;
>> > private Double doubleValue;
>> >
>> > public Bean(Long longValue, Double doubleValue) {
>> > this.longValue = longValue;
>> > this.doubleValue = doubleValue;
>> > }
>> >
>> > @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include =
>> JsonTypeInfo.As.PROPERTY, property = "@class")
>> > public Map<String, Object> getData() {
>> > Map<String, Object> map = new HashMap<>();
>> > map.put("long", longValue);
>> > map.put("double", doubleValue);
>> > return map;
>> > }
>> > }
>> >
>> > public static class MapHolder {
>> > public Map<String, Object> data;
>> > MapHolder() {}
>> > }
>> > }
>> >
>> >
>> >
>> > --
>> > You received this message because you are subscribed to the Google
>> Groups "jackson-user" group.
>> > To unsubscribe from this group and stop receiving emails from it, send
>> an email to [email protected].
>> > To post to this group, send email to [email protected].
>> > For more options, visit https://groups.google.com/d/optout.
>>
>
--
You received this message because you are subscribed to the Google Groups
"jackson-user" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To post to this group, send email to [email protected].
For more options, visit https://groups.google.com/d/optout.