How to preserve the offset while deserializing OffsetDateTime with Jackson - java

In an incoming JSON, I have an ISO8601-compliant datetime field, containing zone offset. I'd like to preserve this offset, but unfortunately Jackson defaults to GMT/UTC while deserializing this field (what I understood from http://wiki.fasterxml.com/JacksonFAQDateHandling).
#RunWith(JUnit4.class)
public class JacksonOffsetDateTimeTest {
private ObjectMapper objectMapper;
#Before
public void init() {
objectMapper = Jackson2ObjectMapperBuilder.json()
.modules(new JavaTimeModule())
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.build();
}
#Test
public void test() throws IOException {
final String json = "{ \"date\": \"2000-01-01T12:00:00.000-04:00\" }";
final JsonType instance = objectMapper.readValue(json, JsonType.class);
assertEquals(ZoneOffset.ofHours(-4), instance.getDate().getOffset());
}
}
public class JsonType {
private OffsetDateTime date;
// getter, setter
}
What I'm getting here is:
java.lang.AssertionError: expected:<-04:00> but was:<Z>
How can I make the returned OffsetDateTime to contain the original Offset?
I'm on Jackson 2.8.3.

Change your Object Mapper to this to disable the ADJUST_DATES_TO_CONTEXT_TIME_ZONE.
objectMapper = Jackson2ObjectMapperBuilder.json()
.modules(new JavaTimeModule())
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
.build();

Could you try
objectMapper.configure(SerializationConfig.Feature.WRITE_DATES_AS_TIMESTAMPS, false);
?
According to FAQ you linked, it should provide you with format 1970-01-01T00:00:00.000+0000. This format contains timezone offset (+0000).

Related

How to handle multiple date formats with springboot and jackson

In the json of the post request I have several different date formats. I'm having troubled deserializing all at the same time. I've created a configuration class that will handle one or the other just fine. How do I add additional deserializers to handle the other formats?
I don't have access to the POJO to add any annotations there.
Here's an error I get for one of the dates I'm unable to deserialize
JSON parse error: Cannot deserialize value of type java.time.LocalDateTime from String "09/03/2020 10:59:48": Failed to deserialize java.time.LocalDateTime:
#Configuration
public class JacksonConfig {
#Bean
#Primary
public ObjectMapper objectMapper() {
JavaTimeModule module = new JavaTimeModule();
LocalDateTimeDeserializer localDateTimeDeserializer = new LocalDateTimeDeserializer(
DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss"));
module.addDeserializer(LocalDateTime.class, localDateTimeDeserializer);
return Jackson2ObjectMapperBuilder.json().modules(module)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).build();
}
}
I was able to resolve my issue by overriding the LocalDateTimeDeserializer's deserialize method. I modified the solution from Configure Jackson to parse multiple date formats
public class MultiDateDeserializer extends LocalDateTimeDeserializer {
public MultiDateDeserializer() {
this(null);
}
public MultiDateDeserializer(DateTimeFormatter formatter) {
super(formatter);
}
private static final long serialVersionUID = 1L;
private static final String[] DATE_FORMATS = new String[] { "yyyy-MM-dd'T'HH:mm:ss", "MM/dd/yyyy HH:mm:ss" };
#Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonNode node = p.getCodec().readTree(p);
final String date = node.textValue();
for (String DATE_FORMAT : DATE_FORMATS) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT, Locale.ROOT);
try {
return LocalDateTime.parse(date, formatter);
} catch (DateTimeParseException e) {
}
}
throw new ParseException(0,
"Unparseable date: \"" + date + "\". Supported formats: " + Arrays.toString(DATE_FORMATS));
}
}
And then in my JacksonConfig I have...
#Configuration
public class JacksonConfig {
#Bean
#Primary
public ObjectMapper objectMapper() {
JavaTimeModule module = new JavaTimeModule();
MultiDateDeserializer multiDateDeserializer = new MultiDateDeserializer();
module.addDeserializer(LocalDateTime.class, multiDateDeserializer);
return Jackson2ObjectMapperBuilder.json().modules(module)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).build();
}
}

Cannot deserialize value of type `java.time.Instant` - jackson

Having class like this
#AllArgsConstructor
#NoArgsConstructor
#Getter
#Setter
public final class ActiveRecoveryProcess {
private UUID recoveryId;
private Instant startedAt;
}
I'm getting com.fasterxml.jackson.databind.exc.InvalidFormatException with message Cannot deserialize value of typejava.time.Instantfrom String "2020-02-22T16:37:23": Failed to deserialize java.time.Instant: (java.time.format.DateTimeParseException) Text '2020-02-22T16:37:23' could not be parsed at index 19
JSON input
{"startedAt": "2020-02-22T16:37:23", "recoveryId": "6f6ee3e5-51c7-496a-b845-1c647a64021e"}
Jackson configuration
#Autowired
void configureObjectMapper(final ObjectMapper mapper) {
mapper.registerModule(new ParameterNamesModule())
.registerModule(new Jdk8Module())
.registerModule(new JavaTimeModule());
mapper.findAndRegisterModules();
}
EDIT
JSON is generated from postgres
jsonb_build_object(
'recoveryId', r.recovery_id,
'startedAt', r.started_at
)
where r.started_at is TIMESTAMP.
The String you're trying to parse, 2020-02-22T16:37:23, doesn't end in Z. Instant expects this as it stands for UTC. It simply cannot be parsed. Concat the String with Z to resolve the issue.
String customInstant = "2020-02-22T16:37:23";
System.out.println("Instant of: " + Instant.parse(customInstant.concat("Z")));
One way to do this is to create a Converter.
public final class NoUTCInstant implements Converter<LocalDateTime, Instant> {
#Override
public Instant convert(LocalDateTime value) {
return value.toInstant(ZoneOffset.UTC);
}
#Override
public JavaType getInputType(TypeFactory typeFactory) {
return typeFactory.constructType(LocalDateTime.class);
}
#Override
public JavaType getOutputType(TypeFactory typeFactory) {
return typeFactory.constructType(Instant.class);
}
}
Then annotate the field.
#JsonDeserialize(converter = NoUTCInstant.class)
private Instant startedAt;

Customizing Databind Json serialization format of MonetaryAmount JSR354 / moneta

I'm trying to Json Serialize a POJO with MonetaryAmount field as a string, but the resulting output does not follow the prescribed shape format.
// org.javamoney:moneta:1.1
// com.fasterxml.jackson.core:jackson-annotations:2.7.0
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonPropertyOrder({
"CBNOT_CHARGEBACK_AMOUNT"
})
public class TestMonetaryAmountJsonSerialization {
#JsonProperty("CBNOT_CHARGEBACK_AMOUNT")
#NotNull
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "¤#,##0.00", locale = "en_US")
private final MonetaryAmount chargebackAmount = Monetary.getDefaultAmountFactory().setCurrency("USD").setNumber(12.50).create();
private static final ObjectMapper mapper = new ObjectMapper();
#Test
public void testThis() throws JsonProcessingException{
String json = mapper.writeValueAsString(this);
System.out.println(json);
Assert.assertEquals("{\"CBNOT_CHARGEBACK_AMOUNT\":\"¤12.50\"}",json);
}
}
OUTPUT: {"CBNOT_CHARGEBACK_AMOUNT":{"currency":{"context":{"empty":false,"providerName":"java.util.Currency"},"defaultFractionDigits":2,"currencyCode":"USD","numericCode":840},"number":12.5,"factory":{"defaultMonetaryContext":{"precision":0,"fixedScale":false,"amountType":"org.javamoney.moneta.Money","maxScale":63,"empty":false,"providerName":null},"maxNumber":null,"minNumber":null,"amountType":"org.javamoney.moneta.Money","maximalMonetaryContext":{"precision":0,"fixedScale":false,"amountType":"org.javamoney.moneta.Money","maxScale":-1,"empty":false,"providerName":null}},"context":{"precision":0,"fixedScale":false,"amountType":"org.javamoney.moneta.Money","maxScale":63,"empty":false,"providerName":null},"numberStripped":12.5,"zero":false,"negative":false,"negativeOrZero":false,"positive":true,"positiveOrZero":true}}
Any ideas what am I doing wrong? I threw the kitchen sink in this code here, only for illustrative purposes and compact presentation.
JsonFormat is an annotation used in several (de)serializers defined by Jackson (e.g. DateTimeSerializerBase, NumberSerializers.Base and some other, full list here), it's not a general purpose mechanism turning any object into a string:
Unlike most other Jackson annotations, annotation does not have
specific universal interpretation: instead, effect depends on datatype
of property being annotated (or more specifically, deserializer and
serializer being used).
Specifying it won't have any effect unless you create a custom serializer for MonetaryAmount or use one that makes use of this annotation (and also its pattern property), but if you create a custom serializer, chances are you won't need that level of flexibility as to specify different patterns for different fields and could just use a fixed MonetaryAmountFormat or build the necessary string from the MonetaryAmount object otherwise.
For example
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonPropertyOrder({
"CBNOT_CHARGEBACK_AMOUNT"
})
public class TestMonetaryAmountJsonSerialization {
#JsonProperty("CBNOT_CHARGEBACK_AMOUNT")
#NotNull
private final MonetaryAmount chargebackAmount = Monetary.getDefaultAmountFactory().setCurrency("USD").setNumber(12.50).create();
private static final ObjectMapper mapper = new ObjectMapper();
static {
SimpleModule monetaryModule = new SimpleModule();
monetaryModule.addSerializer(MonetaryAmount.class, new MonetaryAmountSerializer());
mapper.registerModule(monetaryModule);
}
#Test
public void testThis() throws JsonProcessingException {
String json = mapper.writeValueAsString(this);
System.out.println(json);
Assert.assertEquals("{\"CBNOT_CHARGEBACK_AMOUNT\":\"$12.50\"}", json);
}
public static class MonetaryAmountSerializer extends JsonSerializer<MonetaryAmount> {
public void serialize(MonetaryAmount monetaryAmount,
JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException {
StringBuilder sb = new StringBuilder();
MonetaryAmountDecimalFormatBuilder
.of("¤#,##0.00").withCurrencyUnit(monetaryAmount.getCurrency()).build()
.print(sb, monetaryAmount);
jsonGenerator.writeString(sb.toString());
}
}
}

How to make Spring Data Elasticsearch work with java.time.LocalDateTime for date

I am using Spring Data support for Elasticsearch. Here is the timestamp field mapping:
#Field(type = FieldType.Date, index = FieldIndex.not_analyzed, store = true,
format = DateFormat.custom, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZZ")
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern ="yyyy-MM-dd'T'HH:mm:ss.SSSZZ")
private LocalDateTime timestamp;
This results in mapping of the field in Elasticsearch as follows:
"timestamp":{"type":"date","store":true,"format":"yyyy-MM-dd'T'HH:mm:ss.SSSZZ"}
When I use java.util.Date instead everything works fine. However, when I switch to java.time.LocalDateTime as above the document sent to Elasticsearch causes an exception. Here is the document (timestamp field only for brevity):
"timestamp": {
"hour":7, "minute":56, "second":9, "nano":147000000, "year":2017, "month":"FEBRUARY",
"dayOfMonth":13, "dayOfWeek":"MONDAY", "dayOfYear":44, "monthValue":2, "chronology": {
"id":"ISO", "calendarType": "iso8601"
}
}
And the exception:
MapperParsingException[failed to parse [timestamp]]; nested: IllegalArgumentException[unknown property [hour]];
(...)
Caused by: java.lang.IllegalArgumentException: unknown property [hour]
It looks like the pattern is being ignored here when jsonizing the document. Any possible tips? Or perhaps you might know how to use the "built-in" _timestamp field with Spring Data?
Check https://github.com/spring-projects/spring-data-elasticsearch/wiki/Custom-ObjectMapper to add JavaTimeModule to your ObjectMapper.
#Configuration
public class ElasticSearchConfiguration {
#Bean
public ElasticsearchTemplate elasticsearchTemplate(Client client) {
return new ElasticsearchTemplate(client, new CustomEntityMapper());
}
public static class CustomEntityMapper implements EntityMapper {
private final ObjectMapper objectMapper;
public CustomEntityMapper() {
objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
objectMapper.registerModule(new CustomGeoModule());
objectMapper.registerModule(new JavaTimeModule());
}
#Override
public String mapToString(Object object) throws IOException {
return objectMapper.writeValueAsString(object);
}
#Override
public <T> T mapToObject(String source, Class<T> clazz) throws IOException {
return objectMapper.readValue(source, clazz);
}
}
}
I was experiencing the similar issue: 'Z' in the date value is treated as a character, so the date parse failed. My solution is using custom pattern to make sure the conversion correct:
#Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'")
private LocalDateTime dateField;
And if we don't want to repeat the pattern on different fields again and again, we can try to centralize to the conversion logic. Spring Data Elastic Search provide the custom conversion feature, check here for the example.
Basically we can write a converter from String to LocalDateTime, and put the date pattern over there.

Jackson serialization exception when trying to serialize LocalDateTime

I can't figure why when trying to serialize an object I get an exception which looks related to deserialization. My object has a field which is of joda type LocalDateTime
ObjectMapper mapper = new ObjectMapper();
mapper.writeValueAsString(response));
I got the following exception:
org.codehaus.jackson.map.JsonMappingException: java.lang.String cannot be cast to org.joda.time.LocalDateTime
I am trying to serialize. Why it is trying to convert String value to object? I tried to add custom deserializers, but it does not work.
update More of the exception:
org.codehaus.jackson.map.JsonMappingException: java.lang.String cannot be cast to org.joda.time.LocalDateTime (through reference chain: com.my.AccountDetailResponse["registrationDate"])
at org.codehaus.jackson.map.JsonMappingException.wrapWithPath(JsonMappingException.java:218) ~[jackson-mapper-asl-1.9.13.jar:1.9.13]
at org.codehaus.jackson.map.JsonMappingException.wrapWithPath(JsonMappingException.java:183) ~[jackson-mapper-asl-1.9.13.jar:1.9.13]
at org.codehaus.jackson.map.ser.std.SerializerBase.wrapAndThrow(SerializerBase.java:140) ~[jackson-mapper-asl-1.9.13.jar:1.9.13]
at org.codehaus.jackson.map.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:158) ~[jackson-mapper-asl-1.9.13.jar:1.9.13]
at org.codehaus.jackson.map.ser.BeanSerializer.serialize(BeanSerializer.java:112) ~[jackson-mapper-asl-1.9.13.jar:1.9.13]
at org.codehaus.jackson.map.ser.StdSerializerProvider._serializeValue(StdSerializerProvider.java:610) ~[jackson-mapper-asl-1.9.13.jar:1.9.13]
at org.codehaus.jackson.map.ser.StdSerializerProvider.serializeValue(StdSerializerProvider.java:256) ~[jackson-mapper-asl-1.9.13.jar:1.9.13]
at org.codehaus.jackson.map.ObjectMapper._configAndWriteValue(ObjectMapper.java:2575) ~[jackson-mapper-asl-1.9.13.jar:1.9.13]
at org.codehaus.jackson.map.ObjectMapper.writeValueAsString(ObjectMapper.java:2097) ~[jackson-mapper-asl-1.9.13.jar:1.9.13]
tried to add deserializer:
CustomDeserializerFactory deserializerFactory = new CustomDeserializerFactory();
deserializerFactory.addSpecificMapping(LocalDateTime.class, new CustomLocalDateTimeDeserializer());
ObjectMapper mapper = new ObjectMapper();
mapper.setDeserializerProvider(new StdDeserializerProvider(deserializerFactory));
try {
remoteActionDto.setPayload(mapper.writeValueAsString(response));
} catch (IOException e) {
logger.error("Can not convert response to json!", e);
.....
}
the deserializer itself. I does not convert actually, but only proof of concept:
private static class CustomLocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
#Override
public LocalDateTime deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return new LocalDateTime();
}
}
I found the problem (or actually a colleague of mine did it). It is the most stupid java behaviour I've ever met. The problem was, that the DTO which contained the LocalDateTime field was populated via reflection, and it seems possible to successfully set a value of type String. A class cast exception occurs when you try to use this field (not when it is being set).
public class MyDto {
// trough reflection, the contained object is a java.lang.String
private LocalDateTime myDate;
}
If you ask why this happened - because we haven't configured a converter for LocalDateTime, but for DateTime instead. My colleague used LocalDateTime by mistake and Jackson silently deserialized it as a String
I've written a quick test class to check what you've provided. It seems to run fine and output the following:
{"value":[2014,2,24,13,42,44,745]}
Granted, that may not be the exact format you're looking for, but either way, here is the class:
public class JsonSerialization {
public static void main(final String[] args) {
try {
final Response response = new Response();
final ObjectMapper mapper = new ObjectMapper();
final String value = mapper.writeValueAsString(response);
System.out.println(value);
}
catch (final Exception e) {
e.printStackTrace();
}
}
public static class Response {
LocalDateTime value = new LocalDateTime();
public LocalDateTime getValue() {
return this.value;
}
public void setValue(final LocalDateTime value) {
this.value = value;
}
}
}
I guess this raises the questions:
Do you have getters and setters for your LocalDateTime property (registrationDate)?
Are you sure the error is occurring where you think it is occurring? The exception is just the part about Jackson, where does it say the writeValueAsString method is called within your code?
I know this isn't part of your question, but in 1.9.13 of Jackson, you should register custom (de)serializers like so:
SimpleModule module = new SimpleModule("", new Version(1, 0, 0, "");
module.addDeserializer(LocalDateTime.class, new CustomLocalDateTimeDeserializer());
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
Register your mapper with JodaModule module.
mapper.register(new JodaModule())
Or try with JodaMapper:
import com.fasterxml.jackson.datatype.joda.JodaMapper;
public static void main(String args[]){
DateTime dateTime = new DateTime(12353434);
JodaMapper mapper = new JodaMapper();
try {
serializedString = mapper.writeValueAsString(dateTime);
System.out.println(serializedString);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
Console Output:
12353434

Categories

Resources