【问题标题】:How to deserialize a float value with a localized decimal separator with Jackson如何使用杰克逊反序列化具有本地化小数分隔符的浮点值
【发布时间】:2014-12-30 18:15:26
【问题描述】:

我用Jackson 解析的输入流包含纬度和经度值,如下所示:

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

由于某种原因,服务器使用 逗号 作为小数点分隔符,生成 InvalidFormatException。由于我无法更改服务器输出格式,我想教杰克逊的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;

}

我如何告诉杰克逊这些坐标值来自德国语言环境?


我想a custom deserializer for the specific fields as discussed here 将是要走的路。我起草了这个:

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 的人为您提供合法的资料。
  • @HotLicks 是否因为, 作为小数点分隔符而无效?如果您指的是尾随逗号 - 这是我忘记输入的内容。我已修复它。
  • 逗号小数点不是有效的 JSON。您可以访问 json.org 并查看语法。逗号的出现可能是因为您对服务器的请求以某种方式暗示了语言环境,并且更改该隐含语言环境将解决问题,但否则另一端的人应该修复它。
  • jslint.com 说它是有效的。但是,服务器端不在我的控制之下。
  • 好吧,我想它是有效的,因为这些是字符串,而不是数字。您可以根据需要处理字符串。本身与 JSON 无关。将public float latitude;更改为public String latitude;等

标签: java jackson json-deserialization decimalformat formatexception


【解决方案1】:

我想出了以下解决方案:

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 时它会不起作用,可以在documentation of JsonDeserialize 中找到。读起来好像我应该使用其中一个,但不能同时使用两者。不管怎样,文档还声称 as = 在定义 using = 时将被忽略:

如果 using() 也被使用,它具有优先级(因为它直接指定了反序列化器,而这只会用于定位反序列化器)并且此注释属性的值被忽略。

【讨论】:

  • 有同样的问题,我很惊讶没有整洁的@JsonFormat("###,##") 这样做
【解决方案2】:

考虑到已接受的答案,有一种方法可以摆脱那些 @JsonDeserialize 注释。

您需要在 ObjectMapper 中注册自定义反序列化器。

按照教程from official web-site,您只需执行以下操作:

    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,则有一个更简单的方法。只需在 Configuration 类的某处定义 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()));

【讨论】:

  • 这属于这里吗? and add the custom HttpMessageConverter to the list of WebMvcConfigurerAdapter message converters:
  • 是的,默认情况下 SpringBoot 不考虑 Jackson2ObjectMapperBuilder Bean。如果您查看默认 WebMvcConfigurerAdapter 是如何初始化的,您会注意到它使用 Jackson2ObjectMapperBuilder.json() 静态方法而不是 Bean 注入
  • 这是迄今为止最好的答案
  • 使用自定义杰克逊模块全局使用自定义解串器并不总是符合要求。但是,确实,如果您需要全局应用此反序列化器,这是最好的方法。但是,在某些情况下,对于不同的 json 有效负载(例如 ZonedDateTime 或货币),您拥有具有不同格式的相同信息类型。在这种情况下,我认为自定义模块、自定义 mixin 和自定义字段反序列化器之间的良好平衡,再加上考虑到这种定义的反序列化器的优先级,恕我直言,这是一种接近它的方法
【解决方案3】:

比其他建议的答案更通用的解决方案是提供自定义的DefaultDeserializationContext 到ObjectMapper。

以下实现(受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 将无法解析。

【讨论】:

    猜你喜欢
    • 2012-01-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多