I developed a RESTfull API with Jersey (2.25.1) and my goal is to always return the same JSON even in case of errors.
I then configured a series of exception mappers including one for handling the com.fasterxml.jackson.core.JsonParseException
exception.
In the version of Jersey that I am using, there is already the com.fasterxml.jackson.jaxrs.base.JsonParseExceptionMapper
mapper that handles the JsonParseException
exception.
In the JavaDoc comment of this mapper it is explicitly stated: Note that javax.ws.rs.ext.Provider annotation was include up to Jackson 2.7, but removed from 2.8 (as per [jaxrs-providers#22].
However, the mapper is registered (not by me) and in fact in the test class that you will find below, from the printout performed by an ApplicationEventListener
, this is evident:
ExceptionMapper: org.glassfish.jersey.server.validation.internal.ValidationExceptionMapper
ExceptionMapper: com.fasterxml.jackson.jaxrs.base.JsonParseExceptionMapper
ExceptionMapper: test.StartupProvidersLoggerTest$JsonParseExceptionMapper
ExceptionMapper: com.fasterxml.jackson.jaxrs.base.JsonMappingExceptionMapper
My problem is that the order of registration of the two mappers is completely random. When mine is registered first, my test succeeds (response media type "application/json"), when the "default" mapper is registered first, my test fails (response media type "text/plain").
As I said, I don't understand why the "default" mapper is recorded, in any case I ask you if there is a way to disable it or give priority to mine (or any other solution).
This is my pom.xml
<properties>
<java.version>1.8</java.version>
<jersey.version>2.25.1</jersey.version>
<junit5.version>5.11.2</junit5.version>
</properties>
<dependencies>
<!-- Jersey -->
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet</artifactId>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.ext</groupId>
<artifactId>jersey-bean-validation</artifactId>
<version>${jersey.version}</version>
</dependency>
<!-- Jersey Test -->
<dependency>
<groupId>org.glassfish.jersey.test-framework</groupId>
<artifactId>jersey-test-framework-core</artifactId>
<version>${jersey.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
<version>${jersey.version}</version>
<scope>test</scope>
</dependency>
<!-- JUnit -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit5.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
This is a simple test class that replicates my problem (but it also happens in production)
package test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import javax.inject.Inject;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.monitoring.ApplicationEvent;
import org.glassfish.jersey.server.monitoring.ApplicationEventListener;
import org.glassfish.jersey.server.monitoring.RequestEvent;
import org.glassfish.jersey.server.monitoring.RequestEventListener;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import com.fasterxml.jackson.core.JsonParseException;
import it.upsy.hausmann.hlabahiapi.api.exceptionmapper.AbstractExceptionMapper;
import lombok.Getter;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class JsonParseExceptionMapperTest extends JerseyTest {
@BeforeAll
public void before() throws Exception {
super.setUp();
}
@AfterAll
public void after() throws Exception {
super.tearDown();
}
@Getter
static class Consumer {
private String message;
}
@Path("test")
public static class HelloResource {
@POST
@Path("hello")
public Response hello(Consumer consumer) {
return Response.status(Response.Status.OK)
.entity(Entity.entity("{\"result\": true}", MediaType.APPLICATION_JSON))
.type(MediaType.APPLICATION_JSON)
.build();
}
}
@Provider
public static class StartupProvidersLogger implements ApplicationEventListener {
@Inject
private ServiceLocator serviceLocator;
@Override
public void onEvent(ApplicationEvent event) {
if (event.getType() == ApplicationEvent.Type.INITIALIZATION_FINISHED) {
System.out.println("--- ExceptionMappers at startup ---");
for (ExceptionMapper<?> mapper : serviceLocator.getAllServices(ExceptionMapper.class)) {
System.out.println("ExceptionMapper: " + mapper.getClass().getName());
}
}
}
@Override
public RequestEventListener onRequest(RequestEvent requestEvent) {
return null;
}
}
@Provider
public static class JsonParseExceptionMapper implements ExceptionMapper<JsonParseException> {
protected static final Logger logger = LogManager.getLogger(AbstractExceptionMapper.class);
@Override
public Response toResponse(JsonParseException exception) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Entity.entity("{\"result\": false}", MediaType.APPLICATION_JSON))
.type(MediaType.APPLICATION_JSON)
.build();
}
}
@Override
protected Application configure() {
ResourceConfig config = new ResourceConfig();
config.register(HelloResource.class);
config.register(StartupProvidersLogger.class);
config.register(JsonParseExceptionMapper.class);
return config;
}
@Test
void doTest() {
String consumerJson = "wrongJson";
Response response = target("test").path("hello").request().post(Entity.entity(consumerJson, MediaType.APPLICATION_JSON));
System.out.println("Response: " + response);
System.out.println("Response.MediaType: " + response.getMediaType());
System.out.println("Response.Payload: " + response.readEntity(String.class));
assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
assertEquals(MediaType.APPLICATION_JSON.toString(), response.getMediaType().toString());
}
}
It seems that adding the @Priority(1) annotation on the custom mapper solves the problem. Also the printing of the mappers registered by ApplicationEventListener now reports my mapper as the absolute first (without the annotation, it was always printed after ValidationExceptionMapper)
--- ExceptionMappers at startup ---
ExceptionMapper: test.JsonParseExceptionMapperTest$JsonParseExceptionMapper
ExceptionMapper: org.glassfish.jersey.server.validation.internal.ValidationExceptionMapper
ExceptionMapper: com.fasterxml.jackson.jaxrs.base.JsonParseExceptionMapper
ExceptionMapper: com.fasterxml.jackson.jaxrs.base.JsonMappingExceptionMapper
Updated mapper class
@Provider
@Priority(1) // javax.annotation.Priority
public static class JsonParseExceptionMapper implements ExceptionMapper<JsonParseException> {
@Override
public Response toResponse(JsonParseException exception) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Entity.entity("{\"result\": false}", MediaType.APPLICATION_JSON))
.type(MediaType.APPLICATION_JSON)
.build();
}
}