How to deserialize a float value with a localized decimal separator with Jackson

前端 未结 3 1310
别那么骄傲
别那么骄傲 2020-12-16 22:16

The input stream I am parsing with Jackson contains latitude and longitude values such as here:

{
    \"name\": \"pr         


        
相关标签:
3条回答
  • 2020-12-16 22:47

    With all respect to accepted answer, there is a way to get rid of those @JsonDeserialize annotations.

    You need to register the custom deserializer in the ObjectMapper.

    Following the tutorial from official web-site you just do something like:

        ObjectMapper mapper = new ObjectMapper();
        SimpleModule testModule = new SimpleModule(
                "DoubleCustomDeserializer",
                new com.fasterxml.jackson.core.Version(1, 0, 0, null))
                .addDeserializer(Double.class, new JsonDeserializer<Double>() {
                    @Override
                    public Double deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
                        String valueAsString = jp.getValueAsString();
                        if (StringUtils.isEmpty(valueAsString)) {
                            return null;
                        }
    
                        return Double.parseDouble(valueAsString.replaceAll(",", "\\."));
                    }
                });
        mapper.registerModule(testModule);
    

    If you're using Spring Boot there is a simpler method. Just define the Jackson2ObjectMapperBuilder bean somewhere in your Configuration class:

    @Bean
    public Jackson2ObjectMapperBuilder jacksonBuilder() {
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
    
        builder.deserializerByType(Double.class, new JsonDeserializer<Double>() {
            @Override
            public Double deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
                String valueAsString = jp.getValueAsString();
                if (StringUtils.isEmpty(valueAsString)) {
                    return null;
                }
    
                return Double.parseDouble(valueAsString.replaceAll(",", "\\."));
            }
        });
    
        builder.applicationContext(applicationContext);
        return builder;
    }
    

    and add the custom HttpMessageConverter to the list of WebMvcConfigurerAdapter message converters:

     messageConverters.add(new MappingJackson2HttpMessageConverter(jacksonBuilder().build()));
    
    0 讨论(0)
  • 2020-12-16 22:48

    I came up with the following solution:

    public class FlexibleFloatDeserializer extends JsonDeserializer<Float> {
    
        @Override
        public Float deserialize(JsonParser parser, DeserializationContext context)
                throws IOException {
            String floatString = parser.getText();
            if (floatString.contains(",")) {
                floatString = floatString.replace(",", ".");
            }
            return Float.valueOf(floatString);
        }
    
    }
    

    ...

    public class Product {
    
        @JsonProperty("name")
        public String name;
        @JsonDeserialize(using = FlexibleFloatDeserializer.class)
        @JsonProperty("latitude")
        public float latitude;
        @JsonDeserialize(using = FlexibleFloatDeserializer.class)
        @JsonProperty("longitude")
        public float longitude;
    
    }
    

    Still I wonder why I it does not work when I specify the return value class as as = Float.class as can be found in the documentation of JsonDeserialize. It reads as if I am supposed to use one or the other but not both. Whatsoever, the docs also claim that as = will be ignored when using = is defined:

    if using() is also used it has precedence (since it directly specified deserializer, whereas this would only be used to locate the deserializer) and value of this annotation property is ignored.

    0 讨论(0)
  • 2020-12-16 23:03

    A more general solution than the other proposed answers, which require registering individual deserializers for each type, is to provide a customized DefaultDeserializationContext to ObjectMapper.

    The following implementation (which is inspired by DefaultDeserializationContext.Impl) worked for me:

    class LocalizedDeserializationContext extends DefaultDeserializationContext {
        private final NumberFormat format;
    
        public LocalizedDeserializationContext(Locale locale) {
            // Passing `BeanDeserializerFactory.instance` because this is what happens at
            // 'jackson-databind-2.8.1-sources.jar!/com/fasterxml/jackson/databind/ObjectMapper.java:562'.
            this(BeanDeserializerFactory.instance, DecimalFormat.getNumberInstance(locale));
        }
    
        private LocalizedDeserializationContext(DeserializerFactory factory, NumberFormat format) {
            super(factory, null);
            this.format = format;
        }
    
        private LocalizedDeserializationContext(DefaultDeserializationContext src, DeserializationConfig config, JsonParser parser, InjectableValues values, NumberFormat format) {
            super(src, config, parser, values);
            this.format = format;
        }
    
        @Override
        public DefaultDeserializationContext with(DeserializerFactory factory) {
            return new LocalizedDeserializationContext(factory, format);
        }
    
        @Override
        public DefaultDeserializationContext createInstance(DeserializationConfig config, JsonParser parser, InjectableValues values) {
            return new LocalizedDeserializationContext(this, config, parser, values, format);
        }
    
        @Override
        public Object handleWeirdStringValue(Class<?> targetClass, String value, String msg, Object... msgArgs) throws IOException {
            // This method is called when default deserialization fails.
            if (targetClass == float.class || targetClass == Float.class) {
                return parseNumber(value).floatValue();
            }
            if (targetClass == double.class || targetClass == Double.class) {
                return parseNumber(value).doubleValue();
            }
            // TODO Handle `targetClass == BigDecimal.class`?
            return super.handleWeirdStringValue(targetClass, value, msg, msgArgs);
        }
    
        // Is synchronized because `NumberFormat` isn't thread-safe.
        private synchronized Number parseNumber(String value) throws IOException {
            try {
                return format.parse(value);
            } catch (ParseException e) {
                throw new IOException(e);
            }
        }
    }
    

    Now set up your object mapper with your desired locale:

    Locale locale = Locale.forLanguageTag("da-DK");
    ObjectMapper objectMapper = new ObjectMapper(null,
                                                 null,
                                                 new LocalizedDeserializationContext(locale));
    

    If you use Spring RestTemplate, you can set it up to use objectMapper like so:

    RestTemplate template = new RestTemplate();
    template.setMessageConverters(
        Collections.singletonList(new MappingJackson2HttpMessageConverter(objectMapper))
    );
    

    Note that the value must be represented as a string in the JSON document (i.e. {"number": "2,2"}), since e.g. {"number": 2,2} is not valid JSON and will fail to parse.

    0 讨论(0)
提交回复
热议问题