I have follows object:
@Validated
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@ToString
@Schema(description = "Request")
public final class Request implements Serializable {
private static final long serialVersionUID = 1L;
@JsonProperty("date")
@Schema(description = "Date")
private OffsetDateTime date;
}
And i send this object as rest-controller's response:
@RestController
public class RequestController {
@RequestMapping(
value = "/requests",
produces = {"application/json;charset=UTF-8"},
consumes = {"application/json"},
method = RequestMethod.POST)
public ResponseEntity<Request> get() {
LocalDate date = LocalDate.of(2021, Month.OCTOBER, 22);
OffsetDateTime dateTime = date.atTime(OffsetTime.MAX);
Request request = new Request(dateTime);
return ResponseEntity.ok(request);
}
}
Yet i have configuration:
@Configuration
public class WebConfiguration implements ServletContextInitializer, WebMvcConfigurer {
private final List<FilterRegistration> filterRegistrations;
private final ApplicationContext applicationContext;
public WebConfiguration(List<RestApplicationInstaller> restApplicationInstallers,
List<MonitoringRestApplicationInstaller> monitoringRestApplicationInstallers,
List<FilterRegistration> filterRegistrations,
ApplicationContext applicationContext) {
this.filterRegistrations = filterRegistrations;
this.applicationContext = applicationContext;
}
@Override
public void onStartup(ServletContext servletContext) {
VersionServletInstaller.installServlets(servletContext, getRegisterAsyncService(servletContext));
filterRegistrations.forEach(filterRegistration -> filterRegistration.onApplicationEvent(new ContextRefreshedEvent(applicationContext)));
}
private RegisterAsyncService getRegisterAsyncService(final ServletContext servletContext) {
final WebApplicationContext ctx = getWebApplicationContext(servletContext);
final RegisterAsyncService registerAsyncService = Objects.requireNonNull(ctx).getBean(RegisterAsyncService.class);
registerAsyncService.exec();
return registerAsyncService;
}
@Bean
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer(CustomAnnotationIntrospector customAnnotationIntrospector) {
return builder -> builder.serializationInclusion(NON_NULL)
.annotationIntrospector(customAnnotationIntrospector);
}
}
Ok.
So... I get the date
field in response as:
2021-10-21T23:59:59.999999999-18:00
When i test my controller, i try to get response, deserialize it to Request
object and check matching:
@DirtiesContext
@SpringBootTest(
classes = {WebConfiguration.class, JacksonAutoConfiguration.class},
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(SpringExtension.class)
@EnableWebMvc
class RequestControllerTest {
private static final CharacterEncodingFilter
CHARACTER_ENCODING_FILTER = new CharacterEncodingFilter();
static {
CHARACTER_ENCODING_FILTER.setEncoding(DEFAULT_ENCODING);
CHARACTER_ENCODING_FILTER.setForceEncoding(true);
}
protected MockMvc mockMvc;
@Autowired
protected ObjectMapper objectMapper;
@Autowired
private WebApplicationContext context;
@PostConstruct
private void postConstruct() {
this.mockMvc =
MockMvcBuilders
.webAppContextSetup(this.context)
.addFilters(CHARACTER_ENCODING_FILTER)
.build();
}
@Test
void requestByIdTest() throws Exception {
mockMvc.perform(
MockMvcRequestBuilders.post("/requests")
.characterEncoding(CHARACTER_ENCODING_FILTER)
.contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(
result -> Assertions.assertEquals(mapToObject(result.getResponse().getContentAsString(Charset.forName(CHARACTER_ENCODING_FILTER)), Request.class), getExpectedRequest()));
}
private WebComplianceRequest getExpectedRequest() {
LocalDate date = LocalDate.of(2021, Month.OCTOBER, 22);
OffsetDateTime dateTime = date.atTime(OffsetTime.MAX);
Request request = new Request(dateTime);
}
private <T> T mapToObject(String json, Class<T> targetClass) {
try {
return getReaderForClass(targetClass).readValue(json);
} catch (IOException e) {
throw new RuntimeExsception(e);
}
}
private <T> ObjectReader getReaderForClass(Class<T> targetClass) {
return objectMapper.readerFor(targetClass);
}
}
But i get a exception, because date
field in expected object and in got object are differ:
Date in response: 2021-10-22T17:59:59.999999999Z
Expected date: 2021-10-21T23:59:59.999999999-18:00
Why did this happen?
Why does the Z
appear instead of time zone? Why is the date changed from 2021-10-21
to 2021-10-22
? And how would i can fix it?
I do not get any exception, I get matching failed because dates differ when I match response and expected objects. I just deserialize object with standard ObjectMapper
and check objects matching with equals()
.
CodePudding user response:
What we know so far
- You are creating an
OffsetDateTime
from aLocalDate
adding the maximum offset available (which happens to be-18:00
hours) - this
OffsetDateTime
gets correctly serialized to a JSON value of2021-10-21T23:59:59.999999999-18:00
- when deserialized, the value (as
String
) is2021-10-22T17:59:59.999999999Z
The critical part is not included so far: What happens between 2. and 3.?
Please consider updating your question with everything you know about it.
What we can derive
The values that appear incongruent are basically the same moment in time (Instant
), but represented at an offset of -18:00
at serialization and represented in UTC ( 00:00
or simply Z
). Due to a difference of 18 hours between those moments and due to the fact you created an OffsetDateTime
with OffsetTime.MAX
(which is 23:59:59.999999999-18:00
, the maximum time of day at an offset of -18:00
).
That's why the result you get after deserialization is not wrong, but its representation may not be the desired one.
My guess is that an Instant
is used at the sub-steps between 2. and 3. and the deserialization simply provides date and time in UTC only.
I wouldn't pass any time with a maximum offset to any API if it is not explicitly required. Is it in your situation? Consider adding information about that, too.
What we can do to make the time String
s equal
You can use a different possibility of creating the OffsetDateTime
from the LocalDate
, that is using the maximum time of day without an offset explicitly at UTC:
OffsetDateTime dateTime = OffsetDateTime.of(date, LocalTime.MAX, ZoneOffset.UTC);
This would serialize to 2021-10-21T23:59:59.999999999Z
, you could also represent it as 2021-10-21T23:59:59.999999999 00:00
or similar (I would stick to Z
) and deserialization should return the same value.
In case you receive a String
representation in UTC and you don't have any influence on it, you will have to parse it and change the representation by applying the minimum offset (-18:00
), maybe like this:
String atMinOffset = OffsetDateTime.parse("2021-10-22T17:59:59.999999999Z")
.withOffsetSameInstant(ZoneOffset.MIN)
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
System.out.println(atMinOffset);
Output:
2021-10-21T23:59:59.999999999-18:00
In case you get an OffsetDateTime
as a response and just want to check if it is the same point in time, consider this:
public static void main(String[] args) throws IOException {
OffsetDateTime utcOdt = OffsetDateTime.parse("2021-10-22T17:59:59.999999999Z");
OffsetDateTime minOffsetOdt = OffsetDateTime.parse("2021-10-21T23:59:59.999999999-18:00");
System.out.println("OffsetDateTimes equal? --> " utcOdt.equals(minOffsetOdt));
System.out.println("Instants equal? --> " utcOdt.toInstant().equals(minOffsetOdt.toInstant()));
}
It's output is
OffsetDateTimes equal? --> false
Instants equal? --> true
Why?
An OffsetDateTime
is a representation of a moment in time while an Instant
actually is that moment in time.
That means you should compare the real moment in time instead of context-based representations of it.