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.

Reply via email to