How to handle multiple date formats with springboot and jackson - java

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();
}
}

Related

How to dynamically customise deserialiser for date format?

I am working on custom JSON deserialiser and have the below class
public class yyyy_MM_dd_DateDeserializer extends StdDeserializer <LocalDate> {
public yyyy_MM_dd_DateDeserializer() {
this(null);
}
public yyyy_MM_dd_DateDeserializer(Class t) {
super(t);
}
#Override
public LocalDate deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
String dateString = jsonParser.getText();
LocalDate localDate = null;
try {
localDate = LocalDate.parse(dateString, "yyyy-MM-dd");
} catch (DateTimeParseException ex) {
throw new RuntimeException("Unparsable date: " + dateString);
}
return localDate;
}
}
and in my request class
#Valid
#JsonDeserialize(using = LocalDateDeserializer.class)
#JsonSerialize(using = LocalDateSerializer.class)
private LocalDate endDate;
It works fine but I am wondering if I can pass the date format dynamically. Instead of hardcoding in yyyy_MM_dd_DateDeserializer.
I want to pass the date format from my request class so that my deserialiser is more generic any anyone can use it by sending the required format.
I think you working too hard to get what you want. There is a simpler way without writing your own deserializer. Look at this question. Essentially it looks like
#JsonFormat(shape= JsonFormat.Shape.STRING, pattern="EEE MMM dd HH:mm:ss Z yyyy")
#JsonProperty("created_at")
ZonedDateTime created_at;
And you just put your own mask. Also, I once had a task of parsing date with unknown format, essentially I needed to parse any valid date. Here is an article describing the idea of how to implement it: Java 8 java.time package: parsing any string to date. You might find it useful
Not when using a binder library (The very point of binding is that it is not dynamic.).
But you could when using a simple parsing library such as org.json
When you are working with java.time.* classes and Jackson is good to start from registering JavaTimeModule which comes from jackson-datatype-jsr310 module. We can extend it and register serialiser with provided pattern like in below example:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class JsonApp {
public static void main(String[] args) throws Exception {
ObjectMapper mapperIso = createObjectMapper("yyyy-MM-dd");
ObjectMapper mapperCustom0 = createObjectMapper("yyyy/MM/dd");
ObjectMapper mapperCustom1 = createObjectMapper("MM-dd-yyyy");
System.out.println(mapperIso.writeValueAsString(new Time()));
System.out.println(mapperCustom0.writeValueAsString(new Time()));
System.out.println(mapperCustom1.writeValueAsString(new Time()));
}
private static ObjectMapper createObjectMapper(String pattern) {
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(pattern)));
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(javaTimeModule);
return mapper;
}
}
class Time {
private LocalDate now = LocalDate.now();
public LocalDate getNow() {
return now;
}
public void setNow(LocalDate now) {
this.now = now;
}
#Override
public String toString() {
return "Time{" +
"now=" + now +
'}';
}
}
Aboce code prints:
{"now":"2019-02-24"}
{"now":"2019/02/24"}
{"now":"02-24-2019"}

How to custom a global jackson deserializer for java.time.LocalDateTime

Jackson's JavaTimeModule serialize/deserializejava.time well globally, but its default date-time format is ISO standard, like 2018-01-10T10:20:30 for LocalDateTime and 2018-01-10T10:20:30+08:00 for OffsetDateTime. But I need to set a global local format like 2018-01-10 10:20:30 for LocalDateTime and OffsetDateTime, without T and OffsetTime (use local default OffsetTime). How can I do this?
Notes: I know about #JsonFormat, #JsonSerialize and #JsonDeserialize. That is not global setting.
Spring boot
#SpringBootApplication
public class Application implements Jackson2ObjectMapperBuilderCustomizer {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Override
public void customize(Jackson2ObjectMapperBuilder builder) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTimeSerializer localDateTimeSerializer = new LocalDateTimeSerializer(formatter);
builder.failOnEmptyBeans(false) // prevent InvalidDefinitionException Error
.serializerByType(LocalDateTime.class, localDateTimeSerializer);
}
}
Springboot & Spring Framework
#Configuration
public class WebMvcConfig implements WebMvcConfigurer {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTimeSerializer localDateTimeSerializer = new LocalDateTimeSerializer(formatter);
LocalDateTimeDeserializer localDateTimeDeserializer = new LocalDateTimeDeserializer(formatter);
JavaTimeModule module = new JavaTimeModule();
module.addSerializer(LocalDateTime.class, localDateTimeSerializer);
module.addDeserializer(LocalDateTime.class, localDateTimeDeserializer);
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
// add converter at the very front
// if there are same type mappers in converters, setting in first mapper is used.
converters.add(0, new MappingJackson2HttpMessageConverter(mapper));
}
}
hope this could help you.
If you are using a single instance of ObjectMapper globally, (and want a solution independent of Spring/Java 8 jackson modules,) you can do something like:
public ObjectMapper getCustomConfigMapper() {
final ObjectMapper mapper = new ObjectMapper();
final SimpleModule module = new SimpleModule();
module.addDeserializer(LocalDateTime.class, new CustomLocaDateTimeDeserializer());
mapper.registerModule(module);
return mapper;
}
public static class CustomLocaDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
public static final DateTimeFormatter CUSTOM_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
#Override
public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext __) throws IOException {
final String value = jsonParser.getText().strip();
return LocalDateTime.parse(value, CUSTOM_FORMATTER);
}
}
You can configure an ObjectMapper. This page explains how https://www.baeldung.com/jackson-serialize-dates. I think you want something close to example 4 on that page.
Then you need to make that the global ObjectMapper. Different frameworks use different methods. This page explains how to do that in Spring and Spring-boot Configuring ObjectMapper in Spring. For others just Google for it.

JPA #Past Date/Calendar validation

In a spring-boot based project i have a simple DTO object:
public class ExpenseDTO {
#Min(value = 1, message = "expense.amount.negative")
private int amount;
#Past
private Calendar createdAt;
// setters/getters/constructor are omitted
}
and such rest controller:
public class ExpenseController {
private final ExpenseService expenseService;
#Autowired
public ExpenseController(ExpenseService expenseService) {
this.expenseService = expenseService;
}
#RequestMapping(value = ADD_EXPENSE, method = POST)
public ResponseEntity addExpense(#Valid #RequestBody ExpenseDTO expenseDTO, Principal principal) {
expenseService.addExpense(expenseDTO, principal.getName());
return ResponseEntity.ok().build();
}
}
From a client i'm gonna send a current date: {"createdAt": "2017-01-27T21:32:19.183Z"} but during validation on the back end the date will be parsed as "2017-01-28T01:30:00.000+0200" so the result is wrong and validation fails. I was trying to play around with #JsonFormat but with no result.
NOTE: i'm using H2 db and if remove #Past from the DTO object everything works just fine, but i have to disable future date.
So how can i validate the date without timezone, i mean i need the exactly the same date on the back end as it was send from a client?!
It might be because of the TimeZone set in your local machine. You can set it to UTC in the application startup, e.g.:
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
Also, you need to set the TimeZone in the SimpleDateFormat instance that is being configured inside ObjectMapper (if you are configuring ObjectMapper as a bean), e.g.:
#Bean
public ObjectMapper objectMapper(){
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
objectMapper.setSerializationInclusion(Include.NON_NULL);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss.SSS'Z'");
simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
objectMapper.setDateFormat(simpleDateFormat);
return objectMapper;
}
If it still does not work, I would recommend creating a custom deserializer for Calendar and manually setting TimeZone in new instance, e.g.:
#Component
public class CalendarDeserialiser extends JsonDeserializer<Calendar>{
TimeZone UTC = TimeZone.getTimeZone("UTC");
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss.SSS'Z'");
#Override
public Calendar deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
Calendar calendar;
try{
calendar = Calendar.getInstance(UTC);
calendar.setTime(dateFormat.parse(p.getText()));
}catch(Exception e){
throw new IOException(e);
}
calendar.setTimeInMillis(p.getLongValue());
return calendar;
}
}
And annotate your Calendar field with #JsonDeserialize(using = CalendarDeserialiser.class).

How to preserve the offset while deserializing OffsetDateTime with Jackson

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).

Jackson Serialization Ignore Timezone

I use the below code for serializing the response that get from an external service an return a json response back as part of my service. However when the external service return a time value along with timezone (10:30:00.000-05.00) , jackson is converting it to 15:30:00. How can I ignore the timezone value?
public interface DateFormatMixin {
#JsonFormat(shape=JsonFormat.Shape.STRING, pattern="HH:mm:ss")
public XMLGregorianCalendar getStartTime();
#JsonFormat(shape=JsonFormat.Shape.STRING, pattern="HH:mm:ss")
public XMLGregorianCalendar getEndTime();
}
public ObjectMapper objectMapper() {
com.fasterxml.jackson.databind.ObjectMapper responseMapper = new com.fasterxml.jackson.databind.ObjectMapper();
responseMapper.addMixIn(Time.class, DateFormatMixin.class);
return responseMapper;
}
You can create custom deserializer
public class CustomJsonTimeDeserializerWithoutTimeZone extends JsonDeserializer<Time>{
#Override
public Time deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
DateFormat format = new SimpleDateFormat("hh:mm:ss.SSS");
Time time = null;
try{
Date dt = format.parse("10:30:00.000-05.00".substring(0,12)); // remove incorrect timezone format
return new Time(dt.getTime());
}catch (ParseException e){
e.printStackTrace();
}
}
}
tell jackson to use your custom deserializer
public class Model{
#JsonDeserialize(using = CustomJsonTimeDeserializerWithoutTimeZone.class)
private Time time;
}
and use it like this:
ObjectMapper mapper = new ObjectMapper();
String jsonString = ...// jsonString retrieve from external service
Model model = mapper.readValue(jsonString, Model.class);
You can use Jackson Custom Serialization to add timezone information for your service response
You can create deserializer as below:
public Calendar deserialize(JsonParser jsonParser, DeserializationContext context)
throws IOException, JsonProcessingException {
DateFormat formatter = new SimpleDateFormat(("yyyy-MM-dd'T'HH:mm:ss.SSS"));
String date = jsonParser.getText();
try {
Calendar cal = Calendar.getInstance();
cal.setTime(formatter.parse(date));
return cal;
} catch (ParseException e) {
throw new RuntimeException(e);
}
}

Categories

Resources