如何使用Jackson反序列化本地化小数分隔符的浮点值

12

我正在使用Jackson解析的输入流中包含了类似这样的纬度和经度值:

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

由于某些原因,服务器使用“逗号”作为小数点分隔符,这会导致“InvalidFormatException”。由于我无法更改服务器输出格式,所以我希望教会Jackson的“ObjectMapper”处理这些情况。以下是相关代码:
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;
}

这里是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;

}

我该如何告诉Jackson这些坐标值是使用德语区域设置的?

我认为根据此处讨论,为特定字段编写自定义反序列化器是正确的方法。我起草了以下内容:

public class GermanFloatDeserializer extends JsonDeserializer<Float> {

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

}

那么POJO看起来会像这样:

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;

}

你所拥有的是无效的JSON。请让提供JSON的人提供合法的内容。 - Hot Licks
@HotLicks 是因为使用了,作为小数点而导致无效吗?如果你指的是末尾的逗号,那是我忘记打了。我已经修复了。 - JJD
1
逗号小数点不是有效的JSON。您可以访问json.org并查看语法。可能是因为您对服务器的请求暗示了某个区域设置,从而导致逗号出现,更改暗示的区域设置将解决问题,否则另一端的人应该修复它。 - Hot Licks
jslint.com 表示它是有效的。然而,服务器端不在我的控制范围内。 - JJD
3
我认为这是有效的,因为那些是字符串而不是数字。你可以按照自己的方式处理字符串,与JSON本身无关。将“public float latitude;”改为“public String latitude;”等等。 - Hot Licks
3个回答

11

我想出了以下解决方案:

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;

}

我还在想为什么当我将返回值类指定为as = Float.class时,它就无法工作了,正如可以在JsonDeserialize的文档中找到的那样。文档读起来好像是我应该使用其中之一而不是两个。总之,文档还声称当定义了using =时,as = 将被忽略:

如果还使用了using(),则它具有优先级(因为它直接指定反序列化器,而此属性仅用于查找反序列化器),并且忽略此注释属性的值。


我也遇到了同样的问题,而且很惊讶居然没有一个简洁的@JsonFormat("###,##")可以解决这个问题。 - psp

2
尊重已接受的答案,有一种方法可以摆脱那些@JsonDeserialize注释。
您需要在ObjectMapper中注册自定义反序列化程序。
按照官方网站上的教程,只需执行以下操作:
    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);

如果你正在使用Spring Boot,有一种更简单的方法。只需要在你的配置类中定义Jackson2ObjectMapperBuilder bean即可:
@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;
}

并将自定义的HttpMessageConverter添加到WebMvcConfigurerAdapter消息转换器列表中:
 messageConverters.add(new MappingJackson2HttpMessageConverter(jacksonBuilder().build()));

这个应该放在这里吗?并将自定义的HttpMessageConverter添加到WebMvcConfigurerAdapter消息转换器列表中: - JJD
是的,SpringBoot默认情况下不考虑Jackson2ObjectMapperBuilder Bean。如果您查看默认的WebMvcConfigurerAdapter如何初始化,您会注意到它使用Jackson2ObjectMapperBuilder.json()静态方法而不是Bean注入。 - WeMakeSoftware
这是目前为止最好的答案。 - Vinicius
使用自定义反序列化器全局使用自定义Jackson模块并不总是适合的。但是,确实,如果您需要全局应用此反序列化器,则这是最佳方法。 但是,在某些情况下,您可能具有具有不同JSON有效负载格式的相同信息类型,例如ZonedDateTime或货币。在这种情况下,我认为在自定义模块、自定义混合和自定义字段反序列化器之间取得良好平衡,并考虑此类定义的反序列化器的优先级,是一种方法来处理它 - jtonic

2
比其他提出的答案更通用的解决方案是为ObjectMapper提供一个定制的DefaultDeserializationContext。这需要为每个类型注册单独的反序列化器。下面的实现(受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);
        }
    }
}

现在,使用您想要的语言环境设置对象映射器:
Locale locale = Locale.forLanguageTag("da-DK");
ObjectMapper objectMapper = new ObjectMapper(null,
                                             null,
                                             new LocalizedDeserializationContext(locale));

如果您使用Spring的RestTemplate,可以像下面这样设置它来使用objectMapper
RestTemplate template = new RestTemplate();
template.setMessageConverters(
    Collections.singletonList(new MappingJackson2HttpMessageConverter(objectMapper))
);

请注意,在JSON文档中,值必须表示为字符串(例如{"number": "2,2"}),因为{"number": 2,2}这样的格式无效,无法解析成JSON。请保留HTML标签。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接