How to deserialize float value using localized decimal separator using Jackson

The input stream that I am analyzing using 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 that InvalidFormatException an InvalidFormatException . Since I cannot change the output format of the server, I would like to teach Jackson ObjectMapper handle these 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 these coordinate values ​​are German?




I suppose a custom deserializer for specific fields, as discussed here . I wrote 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 POJO will 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; } 
+4
java jackson json-deserialization decimalformat formatexception
Nov 03 '14 at 23:38
source share
3 answers

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; } 

I'm still wondering why this is not working when I specify the return value class as as = Float.class , as can be found in the JsonDeserialize documentation . It reads as if I should use one or the other, but not one or the other. Be that as it may, the docs also claim that as = will be ignored when using = defined:

if () is used, priority is also used (since it directly indicates the deserializer, whereas this will only be used to determine the deserializer), and the value of this annotation property is ignored.

+6
Nov 09 '14 at 11:16
source share

A more general solution than the other suggested answers that require registering separate deserializers for each type is to provide a custom DefaultDeserializationContext before the ObjectMapper .

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

 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 configure your mapper object with the desired language:

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

If you use Spring RestTemplate , you can configure it to use ObjectMapper as follows:

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

Please note that the value must be represented as a string in a JSON document (ie {"number": "2,2"} ), as, for example, {"number": 2,2} invalid JSON and not will understand.

+2
Sep 26 '16 at 13:34
source share

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

You need to register a custom deserializer in ObjectMapper.

Following the tutorial from the official 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 use Spring Boot, there is a simpler method. Just define a 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 a custom HttpMessageConverter to the list of WebMvcConfigurerAdapter message WebMvcConfigurerAdapter :

  messageConverters.add(new MappingJackson2HttpMessageConverter(jacksonBuilder().build())); 
0
May 27 '15 at 12:42
source share



All Articles