diff --git a/spring-reactive-modules/spring-reactive-3/pom.xml b/spring-reactive-modules/spring-reactive-3/pom.xml
index 17c9690157..45dd25794e 100644
--- a/spring-reactive-modules/spring-reactive-3/pom.xml
+++ b/spring-reactive-modules/spring-reactive-3/pom.xml
@@ -64,6 +64,11 @@
org.springframework.session
spring-session-data-redis
+
+ com.squareup.okhttp3
+ mockwebserver
+ 4.12.0
+
diff --git a/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/Application.java b/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/Application.java
new file mode 100644
index 0000000000..0fcdb3a2fb
--- /dev/null
+++ b/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/Application.java
@@ -0,0 +1,13 @@
+package com.baeldung.custom.deserialization;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+}
diff --git a/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/config/CodecCustomizerConfig.java b/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/config/CodecCustomizerConfig.java
new file mode 100644
index 0000000000..ef3eb1e97f
--- /dev/null
+++ b/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/config/CodecCustomizerConfig.java
@@ -0,0 +1,27 @@
+package com.baeldung.custom.deserialization.config;
+
+import org.springframework.boot.web.codec.CodecCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.MediaType;
+import org.springframework.http.codec.CodecConfigurer;
+import org.springframework.http.codec.json.Jackson2JsonDecoder;
+import org.springframework.http.codec.json.Jackson2JsonEncoder;
+import org.springframework.util.MimeType;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+@Configuration
+public class CodecCustomizerConfig {
+
+ @Bean
+ public CodecCustomizer codecCustomizer(ObjectMapper customObjectMapper) {
+ return configurer -> {
+ MimeType mimeType = MimeType.valueOf(MediaType.APPLICATION_JSON_VALUE);
+ CodecConfigurer.CustomCodecs customCodecs = configurer.customCodecs();
+ customCodecs.register(new Jackson2JsonDecoder(customObjectMapper, mimeType));
+ customCodecs.register(new Jackson2JsonEncoder(customObjectMapper, mimeType));
+ };
+ }
+
+}
diff --git a/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/config/CustomDeserializer.java b/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/config/CustomDeserializer.java
new file mode 100644
index 0000000000..2eeb250e0b
--- /dev/null
+++ b/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/config/CustomDeserializer.java
@@ -0,0 +1,21 @@
+package com.baeldung.custom.deserialization.config;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
+
+public class CustomDeserializer extends LocalDateTimeDeserializer {
+ @Override
+ public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException {
+ try {
+ return OffsetDateTime.parse(jsonParser.getText()).atZoneSameInstant(ZoneOffset.UTC).toLocalDateTime();
+ } catch (Exception e) {
+ return super.deserialize(jsonParser, ctxt);
+ }
+ }
+}
diff --git a/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/config/CustomObjectMapper.java b/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/config/CustomObjectMapper.java
new file mode 100644
index 0000000000..9ebd85abf8
--- /dev/null
+++ b/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/config/CustomObjectMapper.java
@@ -0,0 +1,18 @@
+package com.baeldung.custom.deserialization.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+
+@Configuration
+public class CustomObjectMapper {
+ @Bean
+ public ObjectMapper objectMapper() {
+ return new ObjectMapper()
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
+ .registerModule(new JavaTimeModule());
+ }
+}
diff --git a/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/controller/OrderController.java b/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/controller/OrderController.java
new file mode 100644
index 0000000000..20327333f1
--- /dev/null
+++ b/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/controller/OrderController.java
@@ -0,0 +1,37 @@
+package com.baeldung.custom.deserialization.controller;
+
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.baeldung.custom.deserialization.model.OrderResponse;
+import com.baeldung.custom.deserialization.service.ExternalServiceV1;
+import com.baeldung.custom.deserialization.service.ExternalServiceV2;
+
+import reactor.core.publisher.Mono;
+
+@RestController
+public class OrderController {
+
+ private final ExternalServiceV1 externalServiceV1;
+ private final ExternalServiceV2 externalServiceV2;
+
+ public OrderController(ExternalServiceV1 externalServiceV1, ExternalServiceV2 externalServiceV2) {
+ this.externalServiceV1 = externalServiceV1;
+ this.externalServiceV2 = externalServiceV2;
+ }
+
+ @GetMapping(value = "v1/order/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
+ public final Mono searchOrderV1(@PathVariable(value = "id") int id) {
+ return externalServiceV1.findById(id)
+ .bodyToMono(OrderResponse.class);
+ }
+
+ @GetMapping(value = "v2/order/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
+ public final Mono searchOrderV2(@PathVariable(value = "id") int id) {
+ return externalServiceV2.findById(id)
+ .bodyToMono(OrderResponse.class);
+ }
+
+}
diff --git a/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/model/OrderResponse.java b/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/model/OrderResponse.java
new file mode 100644
index 0000000000..18d45e7dba
--- /dev/null
+++ b/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/model/OrderResponse.java
@@ -0,0 +1,19 @@
+package com.baeldung.custom.deserialization.model;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.UUID;
+
+import lombok.Data;
+
+@Data
+public class OrderResponse {
+
+ private UUID orderId;
+
+ private LocalDateTime orderDateTime;
+
+ private List address;
+
+ private List orderNotes;
+}
diff --git a/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/service/ExternalServiceV1.java b/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/service/ExternalServiceV1.java
new file mode 100644
index 0000000000..34c78ae416
--- /dev/null
+++ b/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/service/ExternalServiceV1.java
@@ -0,0 +1,23 @@
+package com.baeldung.custom.deserialization.service;
+
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.client.WebClient;
+
+@Component
+public class ExternalServiceV1 {
+
+ private final WebClient.Builder webclientBuilder;
+
+ public ExternalServiceV1(WebClient.Builder webclientBuilder) {
+ this.webclientBuilder = webclientBuilder;
+ }
+
+ public WebClient.ResponseSpec findById(int id) {
+ return webclientBuilder.baseUrl("http://localhost:8090/")
+ .build()
+ .get()
+ .uri("external/order/" + id)
+ .retrieve();
+ }
+
+}
diff --git a/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/service/ExternalServiceV2.java b/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/service/ExternalServiceV2.java
new file mode 100644
index 0000000000..0f976a42f2
--- /dev/null
+++ b/spring-reactive-modules/spring-reactive-3/src/main/java/com/baeldung/custom/deserialization/service/ExternalServiceV2.java
@@ -0,0 +1,40 @@
+package com.baeldung.custom.deserialization.service;
+
+import java.time.LocalDateTime;
+
+import org.springframework.http.MediaType;
+import org.springframework.http.codec.json.Jackson2JsonDecoder;
+import org.springframework.http.codec.json.Jackson2JsonEncoder;
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.client.ExchangeStrategies;
+import org.springframework.web.reactive.function.client.WebClient;
+
+import com.baeldung.custom.deserialization.config.CustomDeserializer;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+
+@Component
+public class ExternalServiceV2 {
+
+ public WebClient.ResponseSpec findById(int id) {
+
+ ObjectMapper objectMapper = new ObjectMapper().registerModule(new SimpleModule().addDeserializer(LocalDateTime.class, new CustomDeserializer()));
+
+ WebClient webClient = WebClient.builder()
+ .baseUrl("http://localhost:8090/")
+ .exchangeStrategies(ExchangeStrategies.builder()
+ .codecs(clientDefaultCodecsConfigurer -> {
+ clientDefaultCodecsConfigurer.defaultCodecs()
+ .jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON));
+ clientDefaultCodecsConfigurer.defaultCodecs()
+ .jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON));
+ })
+ .build())
+ .build();
+
+ return webClient.get()
+ .uri("external/order/" + id)
+ .retrieve();
+ }
+
+}
diff --git a/spring-reactive-modules/spring-reactive-3/src/test/java/com/baeldung/custom/deserialization/OrderControllerIntegrationTest.java b/spring-reactive-modules/spring-reactive-3/src/test/java/com/baeldung/custom/deserialization/OrderControllerIntegrationTest.java
new file mode 100644
index 0000000000..9c8bd7c0d4
--- /dev/null
+++ b/spring-reactive-modules/spring-reactive-3/src/test/java/com/baeldung/custom/deserialization/OrderControllerIntegrationTest.java
@@ -0,0 +1,135 @@
+package com.baeldung.custom.deserialization;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.HttpStatus;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.web.reactive.server.WebTestClient;
+
+import com.baeldung.custom.deserialization.model.OrderResponse;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+
+@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
+@TestPropertySource(properties = "server.port=8091")
+@AutoConfigureWebTestClient(timeout = "100000")
+class OrderControllerIntegrationTest {
+
+ @Autowired
+ private WebTestClient webTestClient;
+
+ private static MockWebServer mockExternalService;
+
+ @BeforeAll
+ static void setup() throws IOException {
+ mockExternalService = new MockWebServer();
+ mockExternalService.start(8090);
+ }
+
+ @Test
+ void givenMockedExternalResponse_whenSearchByIdV1_thenOrderResponseShouldFailBecauseOfUnknownProperty() {
+
+ mockExternalService.enqueue(new MockResponse().addHeader("Content-Type", "application/json; charset=utf-8")
+ .setBody("{\n" + " \"orderId\": \"a1b2c3d4-e5f6-4a5b-8c9d-0123456789ab\",\n"
+ + " \"orderDateTime\": \"2024-01-20T12:34:56\",\n"
+ + " \"address\": [\"123 Main St\", \"Apt 456\", \"Cityville\"],\n"
+ + " \"orderNotes\": [\"Special request: Handle with care\", \"Gift wrapping required\"],\n"
+ + " \"customerName\": \"John Doe\",\n" + " \"totalAmount\": 99.99,\n"
+ + " \"paymentMethod\": \"Credit Card\"\n" + " }")
+ .setResponseCode(HttpStatus.OK.value()));
+
+ webTestClient.get()
+ .uri("v1/order/1")
+ .exchange()
+ .expectStatus()
+ .is5xxServerError();
+ }
+
+ @Test
+ void givenMockedExternalResponse_whenSearchByIdV1_thenOrderResponseShouldBeReceivedSuccessfully() {
+
+ mockExternalService.enqueue(new MockResponse().addHeader("Content-Type", "application/json; charset=utf-8")
+ .setBody("{\n" + " \"orderId\": \"a1b2c3d4-e5f6-4a5b-8c9d-0123456789ab\",\n"
+ + " \"orderDateTime\": \"2024-01-20T12:34:56\",\n"
+ + " \"address\": [\"123 Main St\", \"Apt 456\", \"Cityville\"],\n"
+ + " \"orderNotes\": [\"Special request: Handle with care\", \"Gift wrapping required\"]\n"
+ + " }")
+ .setResponseCode(HttpStatus.OK.value()));
+
+ OrderResponse orderResponse = webTestClient.get()
+ .uri("v1/order/1")
+ .exchange()
+ .expectStatus()
+ .isOk()
+ .expectBody(OrderResponse.class)
+ .returnResult()
+ .getResponseBody();
+ assertEquals(UUID.fromString("a1b2c3d4-e5f6-4a5b-8c9d-0123456789ab"), orderResponse.getOrderId());
+ assertEquals(LocalDateTime.of(2024, 1, 20, 12, 34, 56), orderResponse.getOrderDateTime());
+ assertThat(orderResponse.getAddress()).hasSize(3);
+ assertThat(orderResponse.getOrderNotes()).hasSize(2);
+ }
+
+ @Test
+ void givenMockedExternalResponse_whenSearchByIdV2_thenOrderResponseShouldFailBecauseOfUnknownProperty() {
+
+ mockExternalService.enqueue(new MockResponse().addHeader("Content-Type", "application/json; charset=utf-8")
+ .setBody("{\n" + " \"orderId\": \"a1b2c3d4-e5f6-4a5b-8c9d-0123456789ab\",\n"
+ + " \"orderDateTime\": \"2024-01-20T12:34:56\",\n"
+ + " \"address\": [\"123 Main St\", \"Apt 456\", \"Cityville\"],\n"
+ + " \"orderNotes\": [\"Special request: Handle with care\", \"Gift wrapping required\"],\n"
+ + " \"customerName\": \"John Doe\",\n"
+ + " \"totalAmount\": 99.99,\n"
+ + " \"paymentMethod\": \"Credit Card\"\n"
+ + " }")
+ .setResponseCode(HttpStatus.OK.value()));
+
+ webTestClient.get()
+ .uri("v2/order/1")
+ .exchange()
+ .expectStatus()
+ .is5xxServerError();
+ }
+
+ @Test
+ void givenMockedExternalResponse_whenSearchByIdV2_thenOrderResponseShouldBeReceivedSuccessfully() {
+
+ mockExternalService.enqueue(new MockResponse().addHeader("Content-Type", "application/json; charset=utf-8")
+ .setBody("{\n" + " \"orderId\": \"a1b2c3d4-e5f6-4a5b-8c9d-0123456789ab\",\n"
+ + " \"orderDateTime\": \"2024-01-20T14:34:56+01:00\",\n"
+ + " \"address\": [\"123 Main St\", \"Apt 456\", \"Cityville\"],\n"
+ + " \"orderNotes\": [\"Special request: Handle with care\", \"Gift wrapping required\"]\n" + " }")
+ .setResponseCode(HttpStatus.OK.value()));
+
+ OrderResponse orderResponse = webTestClient.get()
+ .uri("v2/order/1")
+ .exchange()
+ .expectStatus()
+ .isOk()
+ .expectBody(OrderResponse.class)
+ .returnResult()
+ .getResponseBody();
+ assertEquals(UUID.fromString("a1b2c3d4-e5f6-4a5b-8c9d-0123456789ab"), orderResponse.getOrderId());
+ assertEquals(LocalDateTime.of(2024, 1, 20, 13, 34, 56), orderResponse.getOrderDateTime());
+ assertThat(orderResponse.getAddress()).hasSize(3);
+ assertThat(orderResponse.getOrderNotes()).hasSize(2);
+ }
+
+ @AfterAll
+ static void tearDown() throws IOException {
+ mockExternalService.shutdown();
+ }
+
+}
\ No newline at end of file