Serializer / Deserializer for OffsetDateTime in Spring Boot - java

I've created a Serializer / Deserializer for OffsetDateTime in a Spring Boot v1.5.14.RELEASE app. First I create a custom constraint annotation:
#Primary
#Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(OffsetDateTime.class, new JsonSerializer<OffsetDateTime>() {
#Override
public void serialize(OffsetDateTime offsetDateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
jsonGenerator.writeString(DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(offsetDateTime));
}
});
simpleModule.addDeserializer(OffsetDateTime.class, new JsonDeserializer<OffsetDateTime>() {
#Override
public OffsetDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
return DateUtils.convertToOffsetDateTime(jsonParser.getValueAsString());
}
});
objectMapper.registerModule(simpleModule);
return objectMapper;
}
in the response I see the value correctly formatted, but on the request I got this error
Failed to convert property value of type 'java.lang.String' to required type 'java.time.OffsetDateTime' for property 'fromDate'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [#com.fasterxml.jackson.annotation.JsonFormat java.time.OffsetDateTime] for value '2019-01-01'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [2019-01-01]]
and
public static OffsetDateTime convertToOffsetDateTime (String date) {
ZoneId zoneId = ZoneId.of(DateFormat.TIME_ZONE_ID);
ZoneOffset currentOffsetForMyZone = zoneId.getRules().getOffset(Instant.now());
return OffsetDateTime.of( parseLocalDate(date),LocalTime.NOON, currentOffsetForMyZone);
}
and I think the Deserializer is not even called because I added this to throw an Exception, but no exception is throw...
public OffsetDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
int m = 9 /0 ;
return DateUtils.convertToOffsetDateTime(jsonParser.getValueAsString());
}
public static LocalDate parseLocalDate(String strDate) {
return LocalDate.parse(strDate, DateFormat.DATE_FORMATTER);
}
and the bean:
#Getter
#Setter
#AllArgsConstructor
#NoArgsConstructor
#Builder
public class HotelData {
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
#DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private OffsetDateTime fromDate;
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
#DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private OffsetDateTime toDate;
}
and
public final class DateFormat {
public static final String DATE_PATTERN = "yyyy-MM-dd";
public static final String TIME_ZONE_ID = "Africa/Brazzaville";
public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter
.ofPattern(DATE_PATTERN)
.withZone(ZoneId.of(TIME_ZONE_ID));
private DateFormat(){}
}
and the problem is on testing:
mockMvc.perform(get("/hotel")
.param("hotelId", "1338767")
.param("fromDate", "2019-01-01")
.param("toDate", "2019-05-21")
.contentType(APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk());

Related

Junit test : Properties value are not resolved in Deserializer class

I'm writing some junit tests for my rest webservice. When I run my test, I got my response from the webservice. So I use objectMapper to map the json to my POJO class. However it fails on this line :
return objectMapper.readValue(json, clazz);
Error : com.fasterxml.jackson.databind.JsonMappingException: zoneId (through reference chain: com.customer.CustomerResponse["customerDetails"]->com.customer.CustomerDetails["licenseDate"])
This is because I'm using this annotation in my POJO class : #JsonDeserialize(using = CustomDateDeserializer.class) and in the Deserializer, I have a variable timezone , where the value is taken from properties file. It seems that the object Mapper is not able to resolve this properties in my Deserializer class :
#Value("${current.timezone}")
private String timezone;
Can you please help how to resolve this issue ? Thanks
#Transactional
#Rollback
class CustomerServiceIntegrationTest {
#Autowired
private DbUnitUtils dbUnitUtils;
protected MockMvc mvc;
#BeforeEach
void setup() throws Exception {
mvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
#BeforeEach
void setupData() throws Exception {
dbUnitUtils.initialiseDataSet(new File("src/test/resources/dbunit/test-dataset.xml"),
DatabaseOperation.CLEAN_INSERT);
}
#Test
void listCustomer_WhenIdExists_Then_ReturnCustomerInfo() throws Exception {
/*
* setup test-data
*/
Long idCustomer= 15L;
String uri = "/customer/" + 15L;
/*
* invoke endpoint being tested
*/
MvcResult mvcResult = mvc.perform(MockMvcRequestBuilders.get(uri).accept(MediaType.APPLICATION_JSON_VALUE))
.andReturn();
/*
* Verification
*/
int status = mvcResult.getResponse().getStatus();
assertEquals(200, status);
CustomerResponse customerResponse =
mapFromJson(mvcResult.getResponse().getContentAsString(), CustomerResponse.class);
assertEquals(idCustomer, customerResponse.getId());
}
protected <T> T mapFromJson(String json, Class<T> clazz)
throws JsonParseException, JsonMappingException, IOException {
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module =
new SimpleModule("CustomDateDeserializer", new Version(1, 0, 0, null, null, null));
module.addDeserializer(ZonedDateTime.class, new CustomDateDeserializer());
objectMapper.registerModule(module);
return objectMapper.readValue(json, clazz);
}
}
public class CustomerResponse{
String name;
String address;
String phoneNo;
CustomerDetails customerDetails;
//getter and setter
}
public class CustomerDetails {
#JsonDeserialize(using = CustomDateDeserializer.class)
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = Utils.FORMAT_DATE)
private ZonedDateTime licenseDate;
//getter and setters
}
public class CustomDateDeserializer extends StdDeserializer<ZonedDateTime> {
#Serial
private static final long serialVersionUID = 1L;
#Value("${current.timezone}")
private String timezone;
public CustomDateDeserializer () {
super((Class<?>) null);
}
protected CustomDateDeserializer (Class<?> vc) {
super(vc);
}
#Override
public ZonedDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws
IOException {
String stringValue = p.getCodec().readValue(p, String.class);
if (StringUtils.isBlank(stringValue)) {
return null;
}
LocalDate localDate = LocalDate.parse(stringValue);
return localDate.atStartOfDay().atZone(ZoneId.of(timezone));
}
}
}

Jackson deserialize date string to Long

Can Java Jackson deserialize a json string date into a Java Long field (milliseconds from epoch)?
This is an example of json field to be deserialized:
"timestamp": "2022-01-02T03:04:05Z",
and this is the same field in the Java class, with the current annotations:
#JsonFormat(shape = JsonFormat.Shape.NUMBER, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC")
#JsonProperty("timestamp")
#JsonPropertyDescription("blah, blah\r\n")
public Long timestamp;
However, an exception happens:
com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot
deserialize value of type java.lang.Long from String
"2022-01-02T06:49:05Z": not a valid Long value
Any hint? Thanks.
The answer by Maurice is correct, it only suffers from using the notoriously troublesome and long outdated SimpleDateFormat and Date classes. Also the deserialize method is much simpler without them:
public class LongTimestampDeserializer extends StdDeserializer<Long> {
public LongTimestampDeserializer() {
this(null);
}
public LongTimestampDeserializer(Class<?> vc) {
super(vc);
}
/** #throws InvalidFormatException If the timestamp cannot be parsed as an Instant */
#Override
public Long deserialize(JsonParser parser, DeserializationContext ctxt)
throws IOException {
String timestamp = parser.getText();
try {
return Instant.parse(timestamp).toEpochMilli();
}
catch (DateTimeParseException dtpe) {
throw new InvalidFormatException(
parser, dtpe.getMessage(), timestamp, Long.class);
}
}
}
The way I understand it the deserializer should throw some subclass of JsonProcessingException in case of a parsing error. InvalidFormatException is a suitable subclass in this case.
Use a custom date deserializer like this one:
public class CustomDateDeserializer extends StdDeserializer<Long> {
private SimpleDateFormat formatter =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
public CustomDateDeserializer() {
this(null);
}
public CustomDateDeserializer(Class<?> vc) {
super(vc);
}
#Override
public Long deserialize(JsonParser jsonparser, DeserializationContext context)
throws IOException, JsonProcessingException {
String date = jsonparser.getText();
try {
return formatter.parse(date).toInstant().toEpochMilli();
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
}
Next annotate your field with #JsonDeserialize(using = CustomDateDeserializer.class).
#JsonDeserialize(using = CustomDateDeserializer.class)
public Long timestamp;

FutureOrPresent not working as expected with present date

Facing this issue as mentioned here. #FutureOrPresent doesn't let present date pass through.
#FutureOrPresent(message = "From Date Must Be Of Future Or Present")
#JsonDeserialize(using = MyCustomDeserializer.class)
#JsonSerialize(using = MyCustomSerializer.class)
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern= "dd-MM-yyyy", timezone ="UTC")
public class MyCustomDeserializer extends JsonDeserializer<Instant> {
private DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd-MM-yyyy").withZone(ZoneOffset.UTC);
#Override
public Instant deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
LocalDate date = LocalDate.parse(p.getText(), fmt);
Instant instant = date.atStartOfDay(ZoneId.of("UTC")).toInstant();
return instant;
}
}
public class MyCustomSerializer extends JsonSerializer<Instant> {
private DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd-MM-yyyy").withZone(ZoneOffset.UTC);
#Override
public void serialize(Instant value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
String str = fmt.format(value);
gen.writeString(str);
}
I get this error:
"errorCode": "BAD_REQUEST", "message": "From Date Must Be Of Future Or Present",
Pojo field has future or present validation but present is not getting applied as expected. Can someone please help understanding the issue?

How i can force JSON dates to not accept integer in java

How i can force JSON date with Java to use a particular pattern and don't accept Integers, for example :
{
"birthday": 1
}
should not be accepted.
I tried
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy")
private LocalDate birthday;
but still accept numbers.
First create a class custom localDate deserializer
public class LocalDateDserializer extends StdDeserializer {
private final SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
public LocalDateDserializer() {
this(null);
}
public LocalDateDserializer(final Class<?> vc) {
super(vc);
}
#Override
public LocalDate deserialize(final JsonParser jsonparser, final DeserializationContext context)
throws IOException, JsonProcessingException {
final String date = jsonparser.getText();
try {
return formatter.parse(date).toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
} catch (final ParseException e) {
throw new RuntimeException(e);
}
}
}
Use the annotation
#JsonDeserialize(using = LocalDateDserializer.class)
private LocalDate birthday;

Spring Boot Cannot Deserialize Object That contanis OffsetDateTime

I'm trying to call a rest endpoint which returns a pojo object which looks like this:
public class Process {
#JsonProperty("id")
private String id = null;
#JsonProperty("processDefinitionId")
private String processDefinitionId = null;
#JsonProperty("businessKey")
private String businessKey = null;
#JsonProperty("startedAt")
private OffsetDateTime startedAt = null;
#JsonProperty("endedAt")
private OffsetDateTime endedAt = null;
#JsonProperty("durationInMs")
private Integer durationInMs = null;
#JsonProperty("startActivityDefinitionId")
private String startActivityDefinitionId = null;
#JsonProperty("endActivityDefinitionId")
private String endActivityDefinitionId = null;
#JsonProperty("startUserId")
private String startUserId = null;
#JsonProperty("deleteReason")
private String deleteReason = null;
//constructors and setters+getters
}
Here is the call:
ResponseEntity<Process> responseModel = restTemplate.exchange("http://localhost:8062/processes", HttpMethod.POST, httpEntity, Process.class);
The problem is that i've tried a few methods like ignoring the OffsetDateTime properties or trying to change the format of that date but it will throw this error:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.threeten.bp.OffsetDateTime` (no Creators, like default construct, exist): no String-argument constructor/factory method to deserialize from String value ('2019-10-04T13:20:29.315Z')
Or it will return null :(
What would be a good solution to solve this?
The error states it cannot construct instance of org.threeten.bp.OffsetDateTime. You need to use
java.time.offsetdatetime
Then in your model you can format it whatever way you like e.g.
#JsonProperty("endedAt") //this line is not needed when it is the same as the instance variable name
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd#HH:mm:ss.SSSZ")
private OffsetDateTime endedAt;
I had the same problem with a bean generated by swagger. To solve it I created some serializer and deserializer for date types: org.threeten.bp.LocalDate and org.threeten.bp.OffsetDateTime. And it works well :).
#Bean
#Primary
public ObjectMapper serializingObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(OffsetDateTime.class, new OffsetDateTimeSerializer());
javaTimeModule.addDeserializer(OffsetDateTime.class, new OffsetDateTimeDeserializer());
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer());
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer());
objectMapper.registerModule(javaTimeModule);
return objectMapper;
}
public static class OffsetDateTimeSerializer extends JsonSerializer<OffsetDateTime> {
#Override
public void serialize(OffsetDateTime arg0, JsonGenerator arg1, SerializerProvider arg2) throws IOException {
arg1.writeString(arg0.toString());
}
}
public static class OffsetDateTimeDeserializer extends JsonDeserializer<OffsetDateTime> {
#Override
public OffsetDateTime deserialize(JsonParser arg0, DeserializationContext arg1) throws IOException {
return OffsetDateTime.parse(arg0.getText());
}
}
public static class LocalDateSerializer extends JsonSerializer<LocalDate> {
#Override
public void serialize(LocalDate arg0, JsonGenerator arg1, SerializerProvider arg2) throws IOException {
arg1.writeString(arg0.toString());
}
}
public static class LocalDateDeserializer extends JsonDeserializer<LocalDate> {
#Override
public LocalDate deserialize(JsonParser arg0, DeserializationContext arg1) throws IOException {
return LocalDate.parse(arg0.getText());
}
}

Categories