java - reglas - separador de miles y decimales
Cómo deserializar un valor flotante con un separador decimal localizado con Jackson (3)
Se me ocurrió la siguiente solución:
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;
}
Todavía me pregunto por qué no funciona cuando especifico la clase de valor devuelto como as = Float.class
como se puede encontrar en la documentación de JsonDeserialize
. Se lee como si se supone que debo usar uno u otro, pero no ambos. En cualquier caso, los documentos también afirman que as =
se ignorará cuando se using =
si también se usa using (), tiene prioridad (ya que especificó el deserializador directamente, mientras que esto solo se usaría para ubicar el deserializador) y se ignora el valor de esta propiedad de anotación.
La secuencia de entrada que estoy analizando con Jackson contiene valores de latitud y longitud, como aquí:
{
"name": "product 23",
"latitude": "52,48264",
"longitude": "13,31822"
}
Por alguna razón, el servidor usa comas como el separador decimal que produce una InvalidFormatException
. Como no puedo cambiar el formato de salida del servidor, me gustaría enseñar a ObjectMapper
de Jackson a manejar esos casos. Aquí está el código relevante:
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;
}
Y aquí está el 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;
}
¿Cómo puedo decirle a Jackson que esos valores de coordenadas vienen con una configuración regional alemana?
Supongo que un deserializador personalizado para los campos específicos que se analizan aquí sería el camino a seguir. Redacté esto:
public class GermanFloatDeserializer extends JsonDeserializer<Float> {
@Override
public Float deserialize(JsonParser parser, DeserializationContext context)
throws IOException {
// TODO Do some comma magic
return floatValue;
}
}
Entonces el POJO se vería así:
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;
}
Con todo respeto a la respuesta aceptada, hay una manera de deshacerse de esas anotaciones @JsonDeserialize
.
Debe registrar el deserializador personalizado en ObjectMapper.
Siguiendo el tutorial del sitio web oficial , simplemente haz algo como:
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);
Si está utilizando Spring Boot, hay un método más simple. Simplemente defina el bean Jackson2ObjectMapperBuilder en algún lugar de su clase de configuración:
@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;
}
y agregue el HttpMessageConverter
personalizado a la lista de convertidores de mensajes WebMvcConfigurerAdapter
:
messageConverters.add(new MappingJackson2HttpMessageConverter(jacksonBuilder().build()));
Una solución más general que las otras respuestas propuestas, que requieren el registro de deserializadores individuales para cada tipo, es proporcionar un DefaultDeserializationContext
personalizado para ObjectMapper
.
La siguiente implementación (que está inspirada en DefaultDeserializationContext.Impl
) funcionó para mí:
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);
}
}
}
Ahora configura tu mapeador de objetos con la configuración regional deseada:
Locale locale = Locale.forLanguageTag("da-DK");
ObjectMapper objectMapper = new ObjectMapper(null,
null,
new LocalizedDeserializationContext(locale));
Si usa Spring RestTemplate
, puede configurarlo para usar objectMapper
manera:
RestTemplate template = new RestTemplate();
template.setMessageConverters(
Collections.singletonList(new MappingJackson2HttpMessageConverter(objectMapper))
);
Tenga en cuenta que el valor debe representarse como una cadena en el documento JSON (es decir, {"number": "2,2"}
), ya que, por ejemplo, {"number": 2,2}
no es JSON válido y no podrá analizarse.