Repository: quarkiverse/quarkus-resteasy-problem
Pull request #483: #482 Support for application/problem+json deserialization for RestClients
Author: login: lwitkowski
Created: 2025-04-03T19:07:21Z | Merged: 2025-04-04T16:51:03Z

### Changes
- [x] jackson deserializer for `HttpProblem`
- [x] jsonb deserializer for `HttpProblem`
- [x] `ThrowingHttpProblemClientExceptionMapper` for RestClients to deserialize `application/problem+json` and throw as `HttpProblem` 
- [x] fix for #429  
- [x] documentation

Closes #482, closes #429, related to #473

Linked issue #482: Support for `application/problem+json` deserialization for RestClients
Extension should register deserializers for HttpProblem type, both for Jackson and JsonB. Should also provide ClientExceptionMapper implementation for `application/problem+json` media type.

related to #473 
will probably also fix #429 

Diff:
diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml
index 62363506..67e16311 100644
--- a/.github/workflows/integration-tests.yaml
+++ b/.github/workflows/integration-tests.yaml
@@ -30,14 +30,14 @@ jobs:
         run: ./mvnw install -Pquick
 
       - name: Integration Tests - Core
-        run: ./mvnw clean verify -P it,${{ matrix.maven_profiles }} -pl 'integration-test/core'
+        run: ./mvnw clean verify -P it,${{ matrix.maven_profiles }} -pl 'integration-test/core,integration-test/rest-client'
 
   test-extensions:
     runs-on: ubuntu-latest
     strategy:
       fail-fast: false
       matrix:
-        it_modules: ["openapi", "metrics", "zalando"]
+        it_modules: ["openapi", "metrics", "zalando"] # rest-client runs along with core as it utilizes same maven profiles
 
     steps:
       - uses: actions/checkout@v4
diff --git a/.github/workflows/native-mode-tests.yaml b/.github/workflows/native-mode-tests.yaml
index ffa564e3..e86e2e97 100644
--- a/.github/workflows/native-mode-tests.yaml
+++ b/.github/workflows/native-mode-tests.yaml
@@ -9,7 +9,7 @@ jobs:
   test-core-native:
     runs-on: ubuntu-latest
     strategy:
-      max-parallel: 4
+      max-parallel: 2
       matrix:
         maven_profiles: ["jackson-classic", "jsonb-classic", "jackson-reactive", "jsonb-reactive"]
 
@@ -34,14 +34,14 @@ jobs:
         run: ./mvnw install -Pquick
 
       - name: Native Tests
-        run: ./mvnw clean verify -P it,native,${{ matrix.maven_profiles }} -pl 'integration-test/core'
+        run: ./mvnw clean verify -P it,native,${{ matrix.maven_profiles }} -pl 'integration-test/core,integration-test/rest-client'
 
   test-extensions-native:
     runs-on: ubuntu-latest
     strategy:
-      max-parallel: 1
+      max-parallel: 2
       matrix:
-        it_modules: ["openapi", "metrics", "zalando"]
+        it_modules: ["openapi", "metrics", "zalando"] # rest-client runs along with core as it utilizes same maven profiles
       fail-fast: false
 
     steps:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 81bbd896..5adc1e6c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -12,7 +12,7 @@ or add your own suggestions and start discussion. Then create fork or branch and
 Quarkus has a strictly enforced code style, so as this extension. Code formatting is done by the Eclipse code formatter, using the config files
 found in the `ide-config` directory. By default when you run `./mvnw install` the code will be formatted automatically.
 
-If you want to run the formatting without doing a full build, you can run `./mvnw process-sources` or `./mvnw formatter:format`.
+If you want to run the formatting without doing a full build, you can run `./mvnw process-sources -P it` or `./mvnw formatter:format -P it`.
 
 More details on how to setup your ide can be found in official [Quarkus Contributing guide](https://github.com/quarkusio/quarkus/blob/main/CONTRIBUTING.md#ide-config-and-code-style)
 
diff --git a/README.md b/README.md
index 2027cd9d..40656b46 100644
--- a/README.md
+++ b/README.md
@@ -188,6 +188,19 @@ Exceptions transformed into http 500s (aka server errors) will be logged as `ERR
 You may also want to check [this article](https://dzone.com/articles/when-http-status-codes-are-not-enough-tackling-web) on RFC7807 practical usage.  
 More on throwing problems: [zalando/problem usage](https://github.com/zalando/problem#usage)
 
+## RestClients (available since <a href="https://github.com/quarkiverse/quarkus-resteasy-problem/releases/tag/3.20.0">v3.20.0</a>)
+If you use RestClients and your upstream services return `application/problem+json` responses, you can register `ThrowingHttpProblemClientExceptionMapper` for your client to get automatic deserialization and rethrowing `HttpProblem` instead of `ClientWebApplicationException` 
+
+```java
+@RegisterRestClient(configKey = "my-rest-client")
+@RegisterProvider(value = ThrowingHttpProblemClientExceptionMapper.class)
+public interface MyRestClient {
+    @GET
+    @Path("/resource")
+    void getResource();
+}
+```
+
 ## OpenAPI integration (available since <a href="https://github.com/quarkiverse/quarkus-resteasy-problem/releases/tag/3.20.0">v3.20.0</a>)
 When `quarkus-smallrye-openapi` is in the classpath, this extension provides a bunch of out-of-the-box features :
 
diff --git a/deployment/src/main/java/io/quarkiverse/resteasy/problem/deployment/ProblemProcessor.java b/deployment/src/main/java/io/quarkiverse/resteasy/problem/deployment/ProblemProcessor.java
index 4658f99b..695bb9a9 100644
--- a/deployment/src/main/java/io/quarkiverse/resteasy/problem/deployment/ProblemProcessor.java
+++ b/deployment/src/main/java/io/quarkiverse/resteasy/problem/deployment/ProblemProcessor.java
@@ -28,6 +28,7 @@
 import io.quarkus.deployment.builditem.IndexDependencyBuildItem;
 import io.quarkus.deployment.builditem.LiveReloadBuildItem;
 import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
+import io.quarkus.jsonb.spi.JsonbDeserializerBuildItem;
 import io.quarkus.jsonb.spi.JsonbSerializerBuildItem;
 import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem;
 import io.quarkus.resteasy.reactive.spi.CustomExceptionMapperBuildItem;
@@ -143,9 +144,12 @@ void registerJacksonItems(BuildProducer<AdditionalBeanBuildItem> additionalBeans
     }
 
     @BuildStep(onlyIf = JsonBDetector.class)
-    void registerJsonbItems(BuildProducer<JsonbSerializerBuildItem> serializers) {
+    void registerJsonbItems(BuildProducer<JsonbSerializerBuildItem> serializers,
+            BuildProducer<JsonbDeserializerBuildItem> deserializers) {
         serializers.produce(
                 new JsonbSerializerBuildItem(EXTENSION_MAIN_PACKAGE + "jsonb.JsonbProblemSerializer"));
+        deserializers.produce(
+                new JsonbDeserializerBuildItem(EXTENSION_MAIN_PACKAGE + "jsonb.JsonbProblemDeserializer"));
     }
 
     /**
diff --git a/integration-test/pom.xml b/integration-test/pom.xml
index f49ceaf0..725f7bfe 100644
--- a/integration-test/pom.xml
+++ b/integration-test/pom.xml
@@ -99,6 +99,7 @@
                 <module>openapi</module>
                 <module>metrics</module>
                 <module>zalando</module>
+                <module>rest-client</module>
             </modules>
         </profile>
 
diff --git a/integration-test/rest-client/pom.xml b/integration-test/rest-client/pom.xml
new file mode 100644
index 00000000..80ef69fb
--- /dev/null
+++ b/integration-test/rest-client/pom.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.quarkiverse.resteasy-problem</groupId>
+        <artifactId>quarkus-resteasy-problem-integration-test</artifactId>
+        <version>999-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>quarkus-resteasy-problem-integration-test-restclient</artifactId>
+    <name>Quarkus - RESTeasy - Problem - Integration Tests - Rest Client</name>
+
+    <profiles>
+        <profile>
+            <id>jackson-classic</id>
+            <activation>
+                <activeByDefault>true</activeByDefault>
+            </activation>
+            <dependencies>
+                <dependency>
+                    <groupId>io.quarkus</groupId>
+                    <artifactId>quarkus-resteasy-jackson</artifactId>
+                </dependency>
+                <dependency>
+                    <groupId>io.quarkus</groupId>
+                    <artifactId>quarkus-resteasy-client-jackson</artifactId>
+                </dependency>
+            </dependencies>
+        </profile>
+        <profile>
+            <id>jackson-reactive</id>
+            <dependencies>
+                <dependency>
+                    <groupId>io.quarkus</groupId>
+                    <artifactId>quarkus-rest-jackson</artifactId>
+                </dependency>
+                <dependency>
+                    <groupId>io.quarkus</groupId>
+                    <artifactId>quarkus-rest-client-jackson</artifactId>
+                </dependency>
+            </dependencies>
+        </profile>
+
+        <profile>
+            <id>jsonb-classic</id>
+            <dependencies>
+                <dependency>
+                    <groupId>io.quarkus</groupId>
+                    <artifactId>quarkus-resteasy-jsonb</artifactId>
+                </dependency>
+                <dependency>
+                    <groupId>io.quarkus</groupId>
+                    <artifactId>quarkus-resteasy-client-jsonb</artifactId>
+                </dependency>
+            </dependencies>
+        </profile>
+        <profile>
+            <id>jsonb-reactive</id>
+            <dependencies>
+                <dependency>
+                    <groupId>io.quarkus</groupId>
+                    <artifactId>quarkus-rest-jsonb</artifactId>
+                </dependency>
+                <dependency>
+                    <groupId>io.quarkus</groupId>
+                    <artifactId>quarkus-rest-client-jsonb</artifactId>
+                </dependency>
+            </dependencies>
+        </profile>
+    </profiles>
+</project>
diff --git a/integration-test/rest-client/src/main/java/io/quarkiverse/resteasy/problem/client/DemoResource.java b/integration-test/rest-client/src/main/java/io/quarkiverse/resteasy/problem/client/DemoResource.java
new file mode 100644
index 00000000..cde10cc1
--- /dev/null
+++ b/integration-test/rest-client/src/main/java/io/quarkiverse/resteasy/problem/client/DemoResource.java
@@ -0,0 +1,47 @@
+package io.quarkiverse.resteasy.problem.client;
+
+import io.quarkiverse.resteasy.problem.HttpProblem;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import org.eclipse.microprofile.rest.client.inject.RestClient;
+
+@Path("/")
+@Produces(MediaType.APPLICATION_JSON)
+public class DemoResource {
+
+    @GET
+    @Path("/throw")
+    @Produces()
+    public Response throwProblem() {
+        throw HttpProblem.builder()
+                .withStatus(409)
+                .withTitle("Conflict from upstream service")
+                .withDetail("Nothing to add")
+                .build();
+    }
+
+    @Inject
+    @RestClient
+    SelfRestClient selfClient;
+
+    @GET
+    @Path("/throw-via-rest-client")
+    public void throwViaRestClient() {
+        selfClient.doThrow();
+    }
+
+    @Inject
+    @RestClient
+    SelfRestClientWithExceptionMapper selfClientWithMapper;
+
+    @GET
+    @Path("/throw-via-rest-client-with-mapper")
+    public void throwViaRestClientWithMapper() {
+        selfClientWithMapper.doThrow();
+    }
+
+}
\ No newline at end of file
diff --git a/integration-test/rest-client/src/main/java/io/quarkiverse/resteasy/problem/client/SelfRestClient.java b/integration-test/rest-client/src/main/java/io/quarkiverse/resteasy/problem/client/SelfRestClient.java
new file mode 100644
index 00000000..5a762b3a
--- /dev/null
+++ b/integration-test/rest-client/src/main/java/io/quarkiverse/resteasy/problem/client/SelfRestClient.java
@@ -0,0 +1,12 @@
+package io.quarkiverse.resteasy.problem.client;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
+
+@RegisterRestClient(configKey = "self")
+public interface SelfRestClient {
+    @GET
+    @Path("/throw")
+    void doThrow();
+}
\ No newline at end of file
diff --git a/integration-test/rest-client/src/main/java/io/quarkiverse/resteasy/problem/client/SelfRestClientWithExceptionMapper.java b/integration-test/rest-client/src/main/java/io/quarkiverse/resteasy/problem/client/SelfRestClientWithExceptionMapper.java
new file mode 100644
index 00000000..8fe1b511
--- /dev/null
+++ b/integration-test/rest-client/src/main/java/io/quarkiverse/resteasy/problem/client/SelfRestClientWithExceptionMapper.java
@@ -0,0 +1,9 @@
+package io.quarkiverse.resteasy.problem.client;
+
+import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
+import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
+
+@RegisterRestClient(configKey = "self-with-mapper")
+@RegisterProvider(value = ThrowingHttpProblemClientExceptionMapper.class)
+public interface SelfRestClientWithExceptionMapper extends SelfRestClient {
+}
\ No newline at end of file
diff --git a/integration-test/rest-client/src/main/resources/application.properties b/integration-test/rest-client/src/main/resources/application.properties
new file mode 100644
index 00000000..dbdfc76a
--- /dev/null
+++ b/integration-test/rest-client/src/main/resources/application.properties
@@ -0,0 +1,5 @@
+quarkus.rest-client.self.url=http://localhost:${quarkus.http.port}/
+%test.quarkus.rest-client.self.url=http://localhost:${quarkus.http.test-port}/
+
+quarkus.rest-client.self-with-mapper.url=http://localhost:${quarkus.http.port}/
+%test.quarkus.rest-client.self-with-mapper.url=http://localhost:${quarkus.http.test-port}/
\ No newline at end of file
diff --git a/integration-test/rest-client/src/test/java/io/quarkiverse/resteasy/problem/client/RestClientIT.java b/integration-test/rest-client/src/test/java/io/quarkiverse/resteasy/problem/client/RestClientIT.java
new file mode 100644
index 00000000..f2dfc74e
--- /dev/null
+++ b/integration-test/rest-client/src/test/java/io/quarkiverse/resteasy/problem/client/RestClientIT.java
@@ -0,0 +1,46 @@
+package io.quarkiverse.resteasy.problem.client;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+import org.junit.jupiter.api.Test;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.CoreMatchers.either;
+import static org.hamcrest.CoreMatchers.equalTo;
+
+@QuarkusTest
+class RestClientIT {
+
+    static {
+        RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
+    }
+
+    @Test
+    void shouldCallItselfViaRestApiWithoutClientExceptionMapper() {
+        String resteasyClassicDetail = "HTTP 409 Conflict";
+        String resteasyReactiveDetail = "Received: 'Conflict, status code 409' when invoking REST Client method: 'io.quarkiverse.resteasy.problem.client.SelfRestClient#doThrow'";
+
+        given()
+                .accept(ContentType.JSON)
+                .get("/throw-via-rest-client")
+                .then()
+                .statusCode(409)
+                .body("title", equalTo("Conflict"))
+                .body("detail", either(equalTo(resteasyClassicDetail)).or(equalTo(resteasyReactiveDetail)))
+                .body("instance", equalTo("/throw-via-rest-client"));
+    }
+
+    @Test
+    void shouldRethrowHttpProblemFromUpstreamServiceWithOverriddenInstance() {
+        given()
+                .accept(ContentType.JSON)
+                .get("/throw-via-rest-client-with-mapper")
+                .then()
+                .statusCode(409)
+                .body("title", equalTo("Conflict from upstream service"))
+                .body("detail", equalTo("Nothing to add"))
+                .body("instance", equalTo("/throw-via-rest-client-with-mapper"));
+    }
+
+}
diff --git a/integration-test/rest-client/src/test/java/io/quarkiverse/resteasy/problem/client/RestClientNativeIT.java b/integration-test/rest-client/src/test/java/io/quarkiverse/resteasy/problem/client/RestClientNativeIT.java
new file mode 100644
index 00000000..49d2250a
--- /dev/null
+++ b/integration-test/rest-client/src/test/java/io/quarkiverse/resteasy/problem/client/RestClientNativeIT.java
@@ -0,0 +1,7 @@
+package io.quarkiverse.resteasy.problem.client;
+
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+
+@QuarkusIntegrationTest
+class RestClientNativeIT extends RestClientIT {
+}
\ No newline at end of file
diff --git a/run-jvm-tests b/run-jvm-tests
index 3a704987..eb670829 100755
--- a/run-jvm-tests
+++ b/run-jvm-tests
@@ -17,8 +17,8 @@ set -e
 
 ./mvnw clean install -Pquick
 
-./mvnw clean verify -P it,jackson-classic -pl 'integration-test/core'
-./mvnw clean verify -P it,jsonb-classic -pl 'integration-test/core'
-./mvnw clean verify -P it,jackson-reactive -pl 'integration-test/core'
-./mvnw clean verify -P it,jsonb-reactive -pl 'integration-test/core'
+./mvnw clean verify -P it,jackson-classic -pl 'integration-test/core,integration-test/rest-client'
+./mvnw clean verify -P it,jsonb-classic -pl 'integration-test/core,integration-test/rest-client'
+./mvnw clean verify -P it,jackson-reactive -pl 'integration-test/core,integration-test/rest-client'
+./mvnw clean verify -P it,jsonb-reactive -pl 'integration-test/core,integration-test/rest-client'
 ./mvnw clean verify -P it -pl 'integration-test/openapi,integration-test/metrics,integration-test/zalando'
diff --git a/run-native-tests b/run-native-tests
index a6a37de7..e694242a 100755
--- a/run-native-tests
+++ b/run-native-tests
@@ -17,8 +17,8 @@ set -x
 
 ./mvnw clean install -Pquick
 
-./mvnw clean verify -P it,native,jackson-classic -pl 'integration-test/core'
-./mvnw clean verify -P it,native,jsonb-classic -pl 'integration-test/core'
-./mvnw clean verify -P it,native,jackson-reactive -pl 'integration-test/core'
-./mvnw clean verify -P it,native,jsonb-reactive -pl 'integration-test/core'
-./mvnw clean verify -P it,native -pl 'integration-test/openapi,integration-test/metrics,integration-test/zalando'
\ No newline at end of file
+./mvnw clean verify -P it,native,jackson-classic -pl 'integration-test/core,integration-test/rest-client'
+./mvnw clean verify -P it,native,jsonb-classic -pl 'integration-test/core,integration-test/rest-client'
+./mvnw clean verify -P it,native,jackson-reactive -pl 'integration-test/core,integration-test/rest-client'
+./mvnw clean verify -P it,native,jsonb-reactive -pl 'integration-test/core,integration-test/rest-client'
+./mvnw clean verify -P it,native -pl 'integration-test/openapi,integration-test/metrics,integration-test/zalando,integration-test/rest-client'
\ No newline at end of file
diff --git a/runtime/pom.xml b/runtime/pom.xml
index b75d291e..7b99ad05 100644
--- a/runtime/pom.xml
+++ b/runtime/pom.xml
@@ -41,6 +41,11 @@
             <artifactId>quarkus-rest-jackson</artifactId>
             <optional>true</optional>
         </dependency>
+        <dependency>
+            <groupId>io.quarkus</groupId>
+            <artifactId>quarkus-rest-client</artifactId>
+            <optional>true</optional>
+        </dependency>
         <dependency>
             <groupId>io.quarkus</groupId>
             <artifactId>quarkus-rest-jsonb</artifactId>
diff --git a/runtime/src/main/java/io/quarkiverse/resteasy/problem/client/ThrowingHttpProblemClientExceptionMapper.java b/runtime/src/main/java/io/quarkiverse/resteasy/problem/client/ThrowingHttpProblemClientExceptionMapper.java
new file mode 100644
index 00000000..ae4d23ef
--- /dev/null
+++ b/runtime/src/main/java/io/quarkiverse/resteasy/problem/client/ThrowingHttpProblemClientExceptionMapper.java
@@ -0,0 +1,30 @@
+package io.quarkiverse.resteasy.problem.client;
+
+import jakarta.ws.rs.core.Response;
+
+import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper;
+
+import io.quarkiverse.resteasy.problem.HttpProblem;
+
+/**
+ * Parses application/problem+json response types and rethrows them as `HttpProblem`.
+ * <p>
+ * Intended to be used as @Provider for RestClients via @RegisterRestClient.
+ */
+public class ThrowingHttpProblemClientExceptionMapper implements ResponseExceptionMapper<RuntimeException> {
+
+    @Override
+    public RuntimeException toThrowable(Response response) {
+        if (!HttpProblem.MEDIA_TYPE.isCompatible(response.getMediaType())) { // TODO add tests here where UTF+8 is appended to respons
+            return null; // Let others handle non-problem formats
+        }
+
+        HttpProblem returnedProblem = response.readEntity(HttpProblem.class);
+
+        // instance must be nullified, otherwise it will be propagated as-is
+        return HttpProblem.builder(returnedProblem)
+                .withInstance(null)
+                .build();
+    }
+
+}
diff --git a/runtime/src/main/java/io/quarkiverse/resteasy/problem/jackson/JacksonProblemDeserializer.java b/runtime/src/main/java/io/quarkiverse/resteasy/problem/jackson/JacksonProblemDeserializer.java
new file mode 100644
index 00000000..381c220c
--- /dev/null
+++ b/runtime/src/main/java/io/quarkiverse/resteasy/problem/jackson/JacksonProblemDeserializer.java
@@ -0,0 +1,64 @@
+package io.quarkiverse.resteasy.problem.jackson;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.Map;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+
+import io.quarkiverse.resteasy.problem.HttpProblem;
+
+/**
+ * Low level Jackson deserializer for HttpProblem type.
+ */
+public final class JacksonProblemDeserializer extends StdDeserializer<HttpProblem> {
+
+    private final ObjectMapper mapper;
+
+    public JacksonProblemDeserializer(ObjectMapper mapper) {
+        super((Class<HttpProblem>) null);
+        this.mapper = mapper;
+    }
+
+    @Override
+    public HttpProblem deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
+        Map<String, Object> rawDeserializedProblem = mapper.readValue(jsonParser, new TypeReference<>() {
+        });
+
+        HttpProblem.Builder builder = HttpProblem.builder();
+        for (String fieldName : rawDeserializedProblem.keySet()) {
+            Object child = rawDeserializedProblem.get(fieldName);
+            switch (fieldName) {
+                case "type" -> builder.withType(uriOrThrow(child, fieldName, jsonParser));
+                case "status" -> builder.withStatus(intOrThrow(child, fieldName, jsonParser));
+                case "title" -> builder.withTitle((String) child);
+                case "detail" -> builder.withDetail((String) child);
+                case "instance" -> builder.withInstance(uriOrThrow(child, fieldName, jsonParser));
+                default -> builder.with(fieldName, child);
+            }
+        }
+        return builder.build();
+    }
+
+    private URI uriOrThrow(Object child, String fieldName, JsonParser jsonParser) throws JsonMappingException {
+        try {
+            return URI.create((String) child);
+        } catch (IllegalArgumentException e) {
+            throw JsonMappingException.from(jsonParser, "'%s' field must be a valid URI".formatted(fieldName));
+        }
+    }
+
+    private int intOrThrow(Object child, String fieldName, JsonParser jsonParser) throws JsonMappingException {
+        try {
+            return (int) child;
+        } catch (ClassCastException e) {
+            throw JsonMappingException.from(jsonParser, "'%s' field must be a valid http status code".formatted(fieldName));
+        }
+    }
+
+}
diff --git a/runtime/src/main/java/io/quarkiverse/resteasy/problem/jackson/JacksonProblemModuleRegistrar.java b/runtime/src/main/java/io/quarkiverse/resteasy/problem/jackson/JacksonProblemModuleRegistrar.java
index c6af164a..db85b1e3 100644
--- a/runtime/src/main/java/io/quarkiverse/resteasy/problem/jackson/JacksonProblemModuleRegistrar.java
+++ b/runtime/src/main/java/io/quarkiverse/resteasy/problem/jackson/JacksonProblemModuleRegistrar.java
@@ -13,8 +13,9 @@ public final class JacksonProblemModuleRegistrar implements ObjectMapperCustomiz
 
     @Override
     public void customize(ObjectMapper mapper) {
-        SimpleModule module = new SimpleModule("RFC7807 problem");
-        module.addSerializer(HttpProblem.class, new JacksonProblemSerializer());
+        SimpleModule module = new SimpleModule("RFC7807 problem")
+                .addSerializer(HttpProblem.class, new JacksonProblemSerializer())
+                .addDeserializer(HttpProblem.class, new JacksonProblemDeserializer(mapper));
         mapper.registerModule(module);
     }
 
diff --git a/runtime/src/main/java/io/quarkiverse/resteasy/problem/jaxrs/WebApplicationExceptionMapper.java b/runtime/src/main/java/io/quarkiverse/resteasy/problem/jaxrs/WebApplicationExceptionMapper.java
index b348faa6..c7c4621e 100644
--- a/runtime/src/main/java/io/quarkiverse/resteasy/problem/jaxrs/WebApplicationExceptionMapper.java
+++ b/runtime/src/main/java/io/quarkiverse/resteasy/problem/jaxrs/WebApplicationExceptionMapper.java
@@ -27,7 +27,9 @@ protected HttpProblem toProblem(WebApplicationException exception) {
 
         Optional.ofNullable(exception.getResponse().getHeaders())
                 .ifPresent(headers -> {
-                    headers.forEach((header, values) -> values.forEach(value -> problem.withHeader(header, value)));
+                    headers.entrySet().stream()
+                            .filter(entry -> !"content-length".equalsIgnoreCase(entry.getKey()))
+                            .forEach(entry -> entry.getValue().forEach(value -> problem.withHeader(entry.getKey(), value)));
                 });
 
         return problem.build();
diff --git a/runtime/src/main/java/io/quarkiverse/resteasy/problem/jsonb/JsonbExceptionMapper.java b/runtime/src/main/java/io/quarkiverse/resteasy/problem/jsonb/JsonbExceptionMapper.java
index 0e2255b1..92631ae7 100644
--- a/runtime/src/main/java/io/quarkiverse/resteasy/problem/jsonb/JsonbExceptionMapper.java
+++ b/runtime/src/main/java/io/quarkiverse/resteasy/problem/jsonb/JsonbExceptionMapper.java
@@ -14,6 +14,6 @@ public final class JsonbExceptionMapper extends ExceptionMapperBase<JsonbExcepti
 
     @Override
     protected HttpProblem toProblem(JsonbException exception) {
-        return HttpProblem.valueOf(BAD_REQUEST, exception.getCause().getMessage());
+        return HttpProblem.valueOf(BAD_REQUEST, exception.getCause() == null ? null : exception.getCause().getMessage());
     }
 }
diff --git a/runtime/src/main/java/io/quarkiverse/resteasy/problem/jsonb/JsonbProblemDeserializer.java b/runtime/src/main/java/io/quarkiverse/resteasy/problem/jsonb/JsonbProblemDeserializer.java
new file mode 100644
index 00000000..3dcfacf7
--- /dev/null
+++ b/runtime/src/main/java/io/quarkiverse/resteasy/problem/jsonb/JsonbProblemDeserializer.java
@@ -0,0 +1,55 @@
+package io.quarkiverse.resteasy.problem.jsonb;
+
+import java.lang.reflect.Type;
+import java.math.BigDecimal;
+import java.net.URI;
+import java.util.Map;
+
+import jakarta.json.bind.JsonbException;
+import jakarta.json.bind.serializer.DeserializationContext;
+import jakarta.json.bind.serializer.JsonbDeserializer;
+import jakarta.json.stream.JsonParser;
+
+import io.quarkiverse.resteasy.problem.HttpProblem;
+
+/**
+ * Low level JsonB serializer for Problem type.
+ */
+public final class JsonbProblemDeserializer implements JsonbDeserializer<HttpProblem> {
+
+    @Override
+    public HttpProblem deserialize(JsonParser jsonParser, DeserializationContext deserializationContext, Type type) {
+        Map<String, Object> rawDeserializedProblem = (Map<String, Object>) deserializationContext.deserialize(Map.class,
+                jsonParser);
+
+        HttpProblem.Builder builder = HttpProblem.builder();
+        for (String fieldName : rawDeserializedProblem.keySet()) {
+            Object child = rawDeserializedProblem.get(fieldName);
+            switch (fieldName) {
+                case "type" -> builder.withType(uriOrThrow(child, fieldName));
+                case "status" -> builder.withStatus(intOrThrow(child, fieldName));
+                case "title" -> builder.withTitle((String) child);
+                case "detail" -> builder.withDetail((String) child);
+                case "instance" -> builder.withInstance(uriOrThrow(child, fieldName));
+                default -> builder.with(fieldName, child);
+            }
+        }
+        return builder.build();
+    }
+
+    private URI uriOrThrow(Object child, String fieldName) {
+        try {
+            return URI.create((String) child);
+        } catch (IllegalArgumentException e) {
+            throw new JsonbException("'%s' field must be a valid URI".formatted(fieldName));
+        }
+    }
+
+    private int intOrThrow(Object child, String fieldName) {
+        try {
+            return ((BigDecimal) child).intValue();
+        } catch (ClassCastException e) {
+            throw new JsonbException("'%s' field must be a valid http status code".formatted(fieldName));
+        }
+    }
+}
diff --git a/runtime/src/test/java/io/quarkiverse/resteasy/problem/client/ThrowingHttpProblemClientExceptionMapperTest.java b/runtime/src/test/java/io/quarkiverse/resteasy/problem/client/ThrowingHttpProblemClientExceptionMapperTest.java
new file mode 100644
index 00000000..04b7f9af
--- /dev/null
+++ b/runtime/src/test/java/io/quarkiverse/resteasy/problem/client/ThrowingHttpProblemClientExceptionMapperTest.java
@@ -0,0 +1,68 @@
+package io.quarkiverse.resteasy.problem.client;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import jakarta.ws.rs.core.Response;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import io.quarkiverse.resteasy.problem.HttpProblem;
+
+class ThrowingHttpProblemClientExceptionMapperTest {
+
+    ThrowingHttpProblemClientExceptionMapper mapper = new ThrowingHttpProblemClientExceptionMapper();
+
+    @ParameterizedTest
+    @ValueSource(strings = {
+            "application/problem+json",
+            "application/problem+json;charset=UTF-8"
+    })
+    void shouldMapProblemCompatibleMimeTypesToHttpProblem(String contentType) {
+
+        // given
+        Response nonProblemResponse = Response.status(400)
+                .type(contentType)
+                .entity(
+                        // serializers are not available/registered here in unit tests
+                        HttpProblem.builder()
+                                .withStatus(400)
+                                .withTitle("Bad Request")
+                                .build())
+                .build();
+
+        // when
+        Throwable mappedThrowable = mapper.toThrowable(nonProblemResponse);
+
+        // then
+        assertThat(mappedThrowable)
+                .isInstanceOf(HttpProblem.class)
+                .usingRecursiveComparison()
+                .isEqualTo(
+                        HttpProblem.builder()
+                                .withStatus(400)
+                                .withTitle("Bad Request")
+                                .build());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {
+            "application/json",
+            "application/json;charset=UTF-8",
+            "application/xml",
+            "text/plain;charset=UTF-8"
+    })
+    void shouldSkipNonProblemResponses(String contentType) {
+        // given
+        Response nonProblemResponse = Response.status(400)
+                .type(contentType)
+                .build();
+
+        // when
+        Throwable mappedThrowable = mapper.toThrowable(nonProblemResponse);
+
+        // then
+        assertThat(mappedThrowable).isNull();
+    }
+
+}
diff --git a/runtime/src/test/java/io/quarkiverse/resteasy/problem/jackson/JacksonProblemDeserializerTest.java b/runtime/src/test/java/io/quarkiverse/resteasy/problem/jackson/JacksonProblemDeserializerTest.java
new file mode 100644
index 00000000..956c74cc
--- /dev/null
+++ b/runtime/src/test/java/io/quarkiverse/resteasy/problem/jackson/JacksonProblemDeserializerTest.java
@@ -0,0 +1,137 @@
+package io.quarkiverse.resteasy.problem.jackson;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+
+import io.quarkiverse.resteasy.problem.HttpProblem;
+import io.quarkiverse.resteasy.problem.HttpProblemMother;
+
+class JacksonProblemDeserializerTest {
+
+    ObjectMapper mapper = new ObjectMapper();
+
+    @BeforeEach
+    void setup() {
+        mapper.registerModule(
+                new SimpleModule("RFC7807 problem")
+                        .addSerializer(HttpProblem.class, new JacksonProblemSerializer())
+                        .addDeserializer(HttpProblem.class, new JacksonProblemDeserializer(mapper)));
+
+    }
+
+    @Test
+    void shouldDeserializeStandardFields() throws IOException {
+        HttpProblem deserialized = deserialise(HttpProblemMother.SERIALIZED_BAD_REQUEST_PROBLEM);
+
+        assertThat(deserialized)
+                .usingRecursiveComparison()
+                .isEqualTo(HttpProblemMother.badRequestProblem());
+    }
+
+    @Test
+    void shouldDeserializeAllKnownTypesOfCustomFields() throws IOException {
+        String problem = """
+                    {
+                        "type": "http://tietoevry.com/problem",
+                        "status": 401,
+                        "title": "Something wrong in the dirt",
+                        "detail": "Deep down wrongness, zażółć gęślą jaźń for Håkensth",
+                        "instance": "/api/v1/endpoint",
+                        "custom_numeric_field": 456,
+                        "custom_text_field": "too short",
+                        "custom_array": ["1", "b", "c"],
+                        "custom_object": {
+                            "elo" : "too short"
+                        }
+                    }
+                """;
+
+        HttpProblem deserialized = deserialise(problem);
+
+        assertThat(deserialized.getType()).isEqualTo(URI.create("http://tietoevry.com/problem"));
+        assertThat(deserialized.getStatusCode()).isEqualTo(401);
+        assertThat(deserialized.getTitle()).isEqualTo("Something wrong in the dirt");
+        assertThat(deserialized.getDetail()).isEqualTo("Deep down wrongness, zażółć gęślą jaźń for Håkensth");
+        assertThat(deserialized.getInstance()).isEqualTo(URI.create("/api/v1/endpoint"));
+        assertThat(deserialized.getParameters())
+                .containsEntry("custom_numeric_field", 456)
+                .containsEntry("custom_text_field", "too short")
+                .containsEntry("custom_array", List.of("1", "b", "c"))
+                .containsEntry("custom_object", Map.of("elo", "too short"));
+    }
+
+    @Test
+    void shouldThrowWhenTypeIsNotValidUri() {
+        String problem = """
+                    {
+                        "type": "this is not a valid uri",
+                        "status": 401,
+                        "title": "Something wrong in the dirt"
+                    }
+                """;
+
+        assertThatThrownBy(() -> deserialise(problem))
+                .isInstanceOf(JsonMappingException.class)
+                .hasMessageStartingWith("'type' field must be a valid URI");
+    }
+
+    @Test
+    void shouldThrowWhenInstanceIsNotValidUri() {
+        String problem = """
+                    {
+                        "instance": "this is not a valid uri",
+                        "status": 401,
+                        "title": "Something wrong in the dirt"
+                    }
+                """;
+
+        assertThatThrownBy(() -> deserialise(problem))
+                .isInstanceOf(JsonMappingException.class)
+                .hasMessageStartingWith("'instance' field must be a valid URI");
+    }
+
+    @Test
+    void shouldThrowWhenStatusIsNotInteger() {
+        String problem = """
+                    {
+                        "status": "_401"
+                    }
+                """;
+
+        assertThatThrownBy(() -> deserialise(problem))
+                .isInstanceOf(JsonMappingException.class)
+                .hasMessageStartingWith("'status' field must be a valid http status code");
+    }
+
+    @Test
+    void shouldThrowWhenJsonIsMalformed() {
+        String problem = """
+                    {
+                        "status": 400,
+                    }
+                """;
+
+        assertThatThrownBy(() -> deserialise(problem))
+                .isInstanceOf(JsonParseException.class)
+                .hasMessageStartingWith(
+                        "Unexpected character ('}' (code 125)): was expecting double-quote to start field name");
+    }
+
+    private HttpProblem deserialise(String json) throws IOException {
+        return mapper.readValue(json, HttpProblem.class);
+    }
+
+}
diff --git a/runtime/src/test/java/io/quarkiverse/resteasy/problem/jaxrs/WebApplicationExceptionMapperTest.java b/runtime/src/test/java/io/quarkiverse/resteasy/problem/jaxrs/WebApplicationExceptionMapperTest.java
index 2debd635..f6a8c198 100644
--- a/runtime/src/test/java/io/quarkiverse/resteasy/problem/jaxrs/WebApplicationExceptionMapperTest.java
+++ b/runtime/src/test/java/io/quarkiverse/resteasy/problem/jaxrs/WebApplicationExceptionMapperTest.java
@@ -1,6 +1,7 @@
 package io.quarkiverse.resteasy.problem.jaxrs;
 
 import static io.quarkiverse.resteasy.problem.HttpProblem.MEDIA_TYPE;
+import static jakarta.ws.rs.core.HttpHeaders.CONTENT_LENGTH;
 import static jakarta.ws.rs.core.HttpHeaders.RETRY_AFTER;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatCode;
@@ -72,6 +73,24 @@ void shouldMapRedirectionException() {
                 .hasFieldOrPropertyWithValue("detail", "HTTP 301 Moved Permanently");
     }
 
+    /**
+     * Otherwise servlet uses content length to truncate resulting json
+     *
+     * @see <a href="https://github.com/quarkiverse/quarkus-resteasy-problem/issues/429">MAJOR: Response is truncated when
+     *      calling from one service to another and error happens on backend</a>
+     */
+    @Test
+    void shouldSkipContentLengthHeader() {
+        WebApplicationException exception = new WebApplicationException(
+                Response.status(429)
+                        .header(CONTENT_LENGTH, 9999)
+                        .build());
+
+        Response response = mapper.toResponse(exception);
+
+        assertThat(response.getHeaderString(CONTENT_LENGTH)).isNull();
+    }
+
     @Test
     void shouldHandleNullHeaders() {
         Headers<Object> nullHeaders = null;
diff --git a/runtime/src/test/java/io/quarkiverse/resteasy/problem/jsonb/JsonbProblemDeserializerTest.java b/runtime/src/test/java/io/quarkiverse/resteasy/problem/jsonb/JsonbProblemDeserializerTest.java
new file mode 100644
index 00000000..e7587cc5
--- /dev/null
+++ b/runtime/src/test/java/io/quarkiverse/resteasy/problem/jsonb/JsonbProblemDeserializerTest.java
@@ -0,0 +1,130 @@
+package io.quarkiverse.resteasy.problem.jsonb;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.math.BigDecimal;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.JsonbException;
+
+import org.junit.jupiter.api.Test;
+
+import io.quarkiverse.resteasy.problem.HttpProblem;
+import io.quarkiverse.resteasy.problem.HttpProblemMother;
+
+class JsonbProblemDeserializerTest {
+
+    Jsonb mapper = JsonbBuilder.newBuilder()
+            .withConfig(new JsonbConfig()
+                    .withDeserializers(new JsonbProblemDeserializer()))
+            .build();
+
+    @Test
+    void shouldDeserializeStandardFields() {
+        HttpProblem deserialized = deserialise(HttpProblemMother.SERIALIZED_BAD_REQUEST_PROBLEM);
+
+        assertThat(deserialized)
+                .usingRecursiveComparison()
+                .isEqualTo(HttpProblemMother.badRequestProblem());
+    }
+
+    @Test
+    void shouldDeserializeAllKnownTypesOfCustomFields() {
+        String problem = """
+                    {
+                        "type": "http://tietoevry.com/problem",
+                        "status": 401,
+                        "title": "Something wrong in the dirt",
+                        "detail": "Deep down wrongness, zażółć gęślą jaźń for Håkensth",
+                        "instance": "/api/v1/endpoint",
+                        "custom_numeric_field": 456,
+                        "custom_text_field": "too short",
+                        "custom_array": ["1", "b", "c"],
+                        "custom_object": {
+                            "elo" : "too short"
+                        }
+                    }
+                """;
+
+        HttpProblem deserialized = deserialise(problem);
+
+        assertThat(deserialized.getType()).isEqualTo(URI.create("http://tietoevry.com/problem"));
+        assertThat(deserialized.getStatusCode()).isEqualTo(401);
+        assertThat(deserialized.getTitle()).isEqualTo("Something wrong in the dirt");
+        assertThat(deserialized.getDetail()).isEqualTo("Deep down wrongness, zażółć gęślą jaźń for Håkensth");
+        assertThat(deserialized.getInstance()).isEqualTo(URI.create("/api/v1/endpoint"));
+        assertThat(deserialized.getParameters())
+                .containsEntry("custom_numeric_field", new BigDecimal(456))
+                .containsEntry("custom_text_field", "too short")
+                .containsEntry("custom_array", List.of("1", "b", "c"))
+                .containsEntry("custom_object", Map.of("elo", "too short"));
+    }
+
+    @Test
+    void shouldThrowWhenTypeIsNotValidUri() {
+        String problem = """
+                    {
+                        "type": "this is not a valid uri",
+                        "status": 401,
+                        "title": "Something wrong in the dirt"
+                    }
+                """;
+
+        assertThatThrownBy(() -> deserialise(problem))
+                .isInstanceOf(JsonbException.class)
+                .hasMessageStartingWith("'type' field must be a valid URI");
+    }
+
+    @Test
+    void shouldThrowWhenInstanceIsNotValidUri() {
+        String problem = """
+                    {
+                        "instance": "this is not a valid uri",
+                        "status": 401,
+                        "title": "Something wrong in the dirt"
+                    }
+                """;
+
+        assertThatThrownBy(() -> deserialise(problem))
+                .isInstanceOf(JsonbException.class)
+                .hasMessageStartingWith("'instance' field must be a valid URI");
+    }
+
+    @Test
+    void shouldThrowWhenStatusIsNotInteger() {
+        String problem = """
+                    {
+                        "status": "_401"
+                    }
+                """;
+
+        assertThatThrownBy(() -> deserialise(problem))
+                .isInstanceOf(JsonbException.class)
+                .hasMessageStartingWith("'status' field must be a valid http status code");
+    }
+
+    @Test
+    void shouldThrowWhenJsonIsMalformed() {
+        String problem = """
+                    {
+                        "status": 400,
+                    }
+                """;
+
+        assertThatThrownBy(() -> deserialise(problem))
+                .isInstanceOf(JsonbException.class)
+                .hasMessageStartingWith(
+                        "Internal error: Invalid token=CURLYCLOSE at (line no=3, column no=5, offset=33). Expected tokens are: [STRING]");
+    }
+
+    private HttpProblem deserialise(String json) {
+        return mapper.fromJson(json, HttpProblem.class);
+    }
+
+}


Reviews:
user: login: pazkooda
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T11:20:44Z
user: login: pazkooda
body: Beside one minor thing look really good!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-04T11:31:49Z
user: login: lwitkowski
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T11:42:04Z
user: login: pazkooda
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T13:47:40Z
user: login: melloware
body: This is great!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T14:27:22Z
user: login: lwitkowski
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T16:52:41Z

Review comments:
diff_hunk: @@ -13,9 +13,15 @@ public final class JacksonProblemModuleRegistrar implements ObjectMapperCustomiz
 
     @Override
     public void customize(ObjectMapper mapper) {
-        SimpleModule module = new SimpleModule("RFC7807 problem");
-        module.addSerializer(HttpProblem.class, new JacksonProblemSerializer());
-        mapper.registerModule(module);
+        registerProblemModule(mapper);
+    }
+
+    public static ObjectMapper registerProblemModule(ObjectMapper mapper) {
path: runtime/src/main/java/io/quarkiverse/resteasy/problem/jackson/JacksonProblemModuleRegistrar.java
user: login: pazkooda
body: `public` for tests only 😜 

You made duplication on `uriOrThrow`/`intOrThrow` yet only for sake of single use in test you "expose API 😉 " in Quarkus Bean.

My preference is to duplicate it in test.

But since this PullReq is "so good" this is the only thing I can catch really 😄 

Good job❕ 
created_at: 2025-04-04T11:20:44Z
author_association: COLLABORATOR
diff_hunk: @@ -13,9 +13,15 @@ public final class JacksonProblemModuleRegistrar implements ObjectMapperCustomiz
 
     @Override
     public void customize(ObjectMapper mapper) {
-        SimpleModule module = new SimpleModule("RFC7807 problem");
-        module.addSerializer(HttpProblem.class, new JacksonProblemSerializer());
-        mapper.registerModule(module);
+        registerProblemModule(mapper);
+    }
+
+    public static ObjectMapper registerProblemModule(ObjectMapper mapper) {
path: runtime/src/main/java/io/quarkiverse/resteasy/problem/jackson/JacksonProblemModuleRegistrar.java
user: login: lwitkowski
body: Agree, I took it a bit too far. Reverted this.
created_at: 2025-04-04T11:42:04Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,137 @@
+package io.quarkiverse.resteasy.problem.jackson;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+
+import io.quarkiverse.resteasy.problem.HttpProblem;
+import io.quarkiverse.resteasy.problem.HttpProblemMother;
+
+class JacksonProblemDeserializerTest {
+
+    ObjectMapper mapper = new ObjectMapper();
+
+    @BeforeEach
path: runtime/src/test/java/io/quarkiverse/resteasy/problem/jackson/JacksonProblemDeserializerTest.java
user: login: pazkooda
body: `@BeforeAll` might be good enough...
created_at: 2025-04-04T13:47:40Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,137 @@
+package io.quarkiverse.resteasy.problem.jackson;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+
+import io.quarkiverse.resteasy.problem.HttpProblem;
+import io.quarkiverse.resteasy.problem.HttpProblemMother;
+
+class JacksonProblemDeserializerTest {
+
+    ObjectMapper mapper = new ObjectMapper();
+
+    @BeforeEach
path: runtime/src/test/java/io/quarkiverse/resteasy/problem/jackson/JacksonProblemDeserializerTest.java
user: login: lwitkowski
body: Then both `mapper` and `setup` method would have to be static
created_at: 2025-04-04T16:52:41Z
author_association: COLLABORATOR

PR comments:
user: login: lwitkowski
created_at: 2025-04-04T10:53:14Z
body: [In the fix](https://github.com/quarkiverse/quarkus-resteasy-problem/pull/483/files#diff-f88f11415480b02fa3fe63289e419364aca07695eae873756de2f32887b9673aR31) for #429 I still decided to pass all headers except the one that caused issue ('content-length'), but the more I think about it the more I believe this extension should not pass any header by default (mainly for safety reasons, but it may be also error-prone). 

Maybe we should introduce `quarkus.resteasy.problem.proxy-pass-header` config option (default empty) with explicit list of headers developers decide they want to pass from upstream service to gateway clients. In both scenarios (with `ThrowingHttpProblemClientExceptionMapper` and without).

WDYT?

edit: I've extracted this topic to #485, I suggest we continue the discussion over there. 
author_association: COLLABORATOR
user: login: lwitkowski
created_at: 2025-04-04T11:39:51Z
body: > Beside one minor thing look really good!

Thanks man, would you mind having a look at this comment? https://github.com/quarkiverse/quarkus-resteasy-problem/pull/483#issuecomment-2778311060

edit: followed up #485
author_association: COLLABORATOR
user: login: melloware
created_at: 2025-04-04T14:38:55Z
body: I have archived my Repo and left a note saying that all features have been ported into this extension: 
https://github.com/melloware/quarkus-openapi-problem

![image](https://github.com/user-attachments/assets/250ebaf3-41ab-4975-ac93-66a625fb2b88)

author_association: CONTRIBUTOR