Search code examples
javajacksonjson-deserializationdecimalformatformatexception

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


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;

}

Solution

  • 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.