Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jackson JsonParseExceptionMapper and JsonMappingExceptionMapper shadows custom mapper

My project uses Spring Boot + Jersey 2.

I created custom Jackson mapper for JsonParseException, but it didn't get called, standard Jackson JsonParseExceptionMapper used instead.

My custom mapper:

 package com.rmn.gfc.common.providers;
 import ...

 @Provider
 public class JsonParseExceptionHandler implements ExceptionMapper<JsonParseException> { 
    @Override
    public Response toResponse(JsonParseException exception) {
        return Response
                .status(Response.Status.BAD_REQUEST)
                //  entity ...
                .build();
    }
}

I register my mapper like this:

@Component
public class OrderServiceResourceConfig extends ResourceConfig {
    public OrderServiceResourceConfig() {
        packages("com.rmn.gfc.common.providers");
    }
}

I am sure that mapper registration is fine because other custom mappers from this package is working, but one for JsonParseException is shadowed by Jersey standard JsonParseExceptionMapper.

How could I override standard Jackson JsonParseExceptionMapper with my implementation?

like image 388
Kysil Ivan Avatar asked Aug 03 '17 08:08

Kysil Ivan


2 Answers

You can blame the JacksonFeature

JacksonFeature registers default exception mappers for Jackson exceptions if the JacksonJaxbJsonProvider is not registered. And that's exactly what you don't want.

See the relevant parts of the source code:

// Register Jackson.
if (!config.isRegistered(JacksonJaxbJsonProvider.class)) {
    // add the default Jackson exception mappers
    context.register(JsonParseExceptionMapper.class);
    context.register(JsonMappingExceptionMapper.class);
    ...
}

Spring Boot registers the JacksonFeature

The Spring Boot's JerseyAutoConfiguration class will register the JacksonFeature if it's on the classpath. See the relevant parts of the source code:

@ConditionalOnClass(JacksonFeature.class)
@ConditionalOnSingleCandidate(ObjectMapper.class)
@Configuration
static class JacksonResourceConfigCustomizer {

    ...

    @Bean
    public ResourceConfigCustomizer resourceConfigCustomizer(
            final ObjectMapper objectMapper) {
        addJaxbAnnotationIntrospectorIfPresent(objectMapper);
        return (ResourceConfig config) -> {
            config.register(JacksonFeature.class);
            config.register(new ObjectMapperContextResolver(objectMapper),
                    ContextResolver.class);
        };
    }

    ...
}

Workaround

As a workaround, you can register the JacksonJaxbJsonProvider and then register your custom exception mappers (or just annotate them with @Provider to get automatically discovered by Jersey):

@Component
public class OrderServiceResourceConfig extends ResourceConfig {

    public OrderServiceResourceConfig() {
        packages("com.rmn.gfc.common.providers");
        register(JacksonJaxbJsonProvider.class);
        // Register other providers
    }
}

See what the documentation says about JacksonJaxbJsonProvider:

JSON content type provider automatically configured to use both Jackson and JAXB annotations (in that order of priority). Otherwise functionally same as JacksonJsonProvider.

Alternative solution

Alternatively you can get rid of the jersey-media-json-jackson artifact and use jackson-jaxrs-json-provider instead. With this, you will get rid of JacksonFeature and then you can register your own exception mappers.

It was mentioned in this answer.

What seems to be the correct solution

See the following quote from the JAX-RS 2.1 specification:

4.1.3 Priorities

Application-supplied providers enable developers to extend and customize the JAX-RS runtime. Therefore, an application-supplied provider MUST always be preferred over a pre-packaged one if a single one is required.

Application-supplied providers may be annotated with @Priority. If two or more providers are candidates for a certain task, the one with the highest priority is chosen: the highest priority is defined to be the one with the lowest value in this case. That is, @Priority(1) is higher than @Priority(10). If two or more providers are eligible and have identical priorities, one is chosen in an implementation dependent manner.

The default priority for all application-supplied providers is javax.ws.rs.Priorities.USER. The general rule about priorities is different for filters and interceptors since these providers are collected into chains.

As pointed in Kysil Ivan's answer, write your own exception mapper and give it a high priority, such as 1. If you use auto discovery, just annotate it with @Provider and @Priority.

@Provider
@Priority(1)
public class JsonParseExceptionMapper implements ExceptionMapper<JsonParseException> {
    ...
}

If you manually register your provider, you can give your provider a binding priority:

@ApplicationPath("/")
public class MyResourceConfig extends ResourceConfig {

    public MyResourceConfig() {
        register(JsonParseExceptionMapper.class, 1);
    }
}
like image 106
cassiomolin Avatar answered Sep 27 '22 18:09

cassiomolin


I found solution that is fine for me.
Use javax.annotation.Priority on your custom mapper to make it override Jackson default mapper, e.g.:

@Provider
@Priority(1)
public class JsonParseExceptionHandler implements ExceptionMapper<JsonParseException> {
    // ...
}

OR , if you register JAX-RS components via ResourceConfig, you can specify priority like this:

public class MyResourceConfig extends ResourceConfig {
    public MyResourceConfig() {
        register(JsonMappingExceptionHandler.class, 1);
        register(JsonParseExceptionHandler.class, 1);
        // ...
    }
}

The lower number the highest priority.
javax.ws.rs.Priorities has some predefined constants for priorities.

like image 32
Kysil Ivan Avatar answered Sep 27 '22 17:09

Kysil Ivan