How to deserialize a float value with a localized

2020-02-28 10:53发布

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

{
    "name": "product 23",
    "latitude": "52,48264",
    "longitude": "13,31822"
}

For some reason the server uses commas as the decimal separator which produces an InvalidFormatException. Since I cannot change the server output format I would like to teach Jackson's ObjectMapper to handle those cases. Here is the relevant code:

public static Object getProducts(final String inputStream) {
    ObjectMapper objectMapper = new ObjectMapper();
    try {
        return objectMapper.readValue(inputStream,
                new TypeReference<Product>() {}
        );
    } catch (UnrecognizedPropertyException e) {
        e.printStackTrace();
    } catch (InvalidFormatException e) {
        e.printStackTrace();
    } catch (JsonMappingException e) {
        e.printStackTrace();
    } catch (JsonParseException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}

And here is the POJO:

import com.fasterxml.jackson.annotation.JsonProperty;

public class Product {

    @JsonProperty("name")
    public String name;
    @JsonProperty("latitude")
    public float latitude;
    @JsonProperty("longitude")
    public float longitude;

}

How can I tell Jackson that those coordinate values come with a German locale?


I suppose a custom deserializer for the specific fields as discussed here would be the way to go. I drafted this:

public class GermanFloatDeserializer extends JsonDeserializer<Float> {

    @Override
    public Float deserialize(JsonParser parser, DeserializationContext context)
            throws IOException {
        // TODO Do some comma magic
        return floatValue;
    }

}

Then the POJO would look like this:

import com.fasterxml.jackson.annotation.JsonProperty;

public class Product {

    @JsonProperty("name")
    public String name;
    @JsonDeserialize(using = GermanFloatDeserializer.class, as = Float.class)
    @JsonProperty("latitude")
    public float latitude;
    @JsonDeserialize(using = GermanFloatDeserializer.class, as = Float.class)
    @JsonProperty("longitude")
    public float longitude;

}

3条回答
smile是对你的礼貌
2楼-- · 2020-02-28 11:15

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()));
查看更多
何必那么认真
3楼-- · 2020-02-28 11:16

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.

查看更多
我只想做你的唯一
4楼-- · 2020-02-28 11:34

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.

查看更多
登录 后发表回答