可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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;
}
回答1:
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.
回答2:
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.
回答3:
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()));