From c85cedcc48ffaf658d9d3ce9ff144787b64bc8d9 Mon Sep 17 00:00:00 2001 From: AnonimProgrammer Date: Fri, 15 May 2026 03:12:52 +0400 Subject: [PATCH] fix: Fix appointment time serialization issue with Jackson. --- .../dto/request/BookAppointmentRequest.java | 7 +++- .../FlexibleOffsetDateTimeDeserializer.java | 39 +++++++++++++++++++ .../input/AppointmentRestAdapterTest.java | 18 +++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/ironhack/infra/config/jackson/FlexibleOffsetDateTimeDeserializer.java diff --git a/src/main/java/com/ironhack/application/dto/request/BookAppointmentRequest.java b/src/main/java/com/ironhack/application/dto/request/BookAppointmentRequest.java index 0dda0eb..dd725af 100644 --- a/src/main/java/com/ironhack/application/dto/request/BookAppointmentRequest.java +++ b/src/main/java/com/ironhack/application/dto/request/BookAppointmentRequest.java @@ -3,9 +3,14 @@ import java.time.OffsetDateTime; import java.util.UUID; +import com.ironhack.infra.config.jackson.FlexibleOffsetDateTimeDeserializer; import jakarta.validation.constraints.NotNull; +import tools.jackson.databind.annotation.JsonDeserialize; public record BookAppointmentRequest( @NotNull(message = "Patient ID is required") UUID patientId, @NotNull(message = "Doctor ID is required") UUID doctorId, - @NotNull(message = "Appointment time is required") OffsetDateTime appointmentTime) {} + + @NotNull(message = "Appointment time is required") + @JsonDeserialize(using = FlexibleOffsetDateTimeDeserializer.class) + OffsetDateTime appointmentTime) {} diff --git a/src/main/java/com/ironhack/infra/config/jackson/FlexibleOffsetDateTimeDeserializer.java b/src/main/java/com/ironhack/infra/config/jackson/FlexibleOffsetDateTimeDeserializer.java new file mode 100644 index 0000000..158918b --- /dev/null +++ b/src/main/java/com/ironhack/infra/config/jackson/FlexibleOffsetDateTimeDeserializer.java @@ -0,0 +1,39 @@ +package com.ironhack.infra.config.jackson; + +import java.time.DateTimeException; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.ValueDeserializer; + +/** + * Accepts standard {@link OffsetDateTime} strings, or a local ISO-8601 datetime + * (e.g. {@code 2026-05-21T13:00}) using the JVM default zone. + */ +public class FlexibleOffsetDateTimeDeserializer extends ValueDeserializer { + @Override + public OffsetDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { + String text = p.getValueAsString(); + if (text == null || text.isBlank()) { + return null; + } + text = text.trim(); + try { + return OffsetDateTime.parse(text); + } catch (DateTimeParseException ignored) { + // fall through + } + try { + LocalDateTime local = LocalDateTime.parse(text, DateTimeFormatter.ISO_LOCAL_DATE_TIME); + return local.atZone(ZoneId.systemDefault()).toOffsetDateTime(); + } catch (DateTimeException e) { + throw ctxt.weirdStringException(text, OffsetDateTime.class, e.getMessage()); + } + } +} diff --git a/src/test/java/com/ironhack/infra/adapter/input/AppointmentRestAdapterTest.java b/src/test/java/com/ironhack/infra/adapter/input/AppointmentRestAdapterTest.java index 8ded3ee..8891516 100644 --- a/src/test/java/com/ironhack/infra/adapter/input/AppointmentRestAdapterTest.java +++ b/src/test/java/com/ironhack/infra/adapter/input/AppointmentRestAdapterTest.java @@ -85,6 +85,24 @@ void shouldBookAppointmentSuccessfully() throws Exception { .andExpect(jsonPath("$.data.status").value("SCHEDULED")); } + @Test + @DisplayName("Should book appointment when appointment time is local ISO-8601 without offset") + void shouldBookAppointmentWithLocalIsoDateTime() throws Exception { + String body = """ + { + "patientId": "%s", + "doctorId": "%s", + "appointmentTime": "2099-01-15T14:30" + } + """.formatted(patientId, doctorId); + + mockMvc.perform(post("/v1/appointments") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.data.status").value("SCHEDULED")); + } + @Test @DisplayName("Should return 400 when booking appointment with past time") void shouldReturnBadRequestWhenAppointmentTimeInPast() throws Exception {