Jackson JsonParseExceptionMapper and JsonMappingExceptionMapper shadows custom mapper
Asked Answered
H

2

8

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?

Heliogabalus answered 3/8, 2017 at 8:13 Comment(0)
H
5

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.

Heliogabalus answered 3/8, 2017 at 10:51 Comment(7)
It's a fancy solution!Bootblack
Which version of jersey ?Laryngoscope
@Brice Jersey 2.xBootblack
yes but 2.21, 2.22, ... ? I don't see any documentation on Jersey on that, and as far as I know this isn't there yet, see jersey/jersey#2994Laryngoscope
I have jackson-core v. 2.8.6Heliogabalus
Adding the Priotiry(1) is not working for me. The only solution is to add the JacksonJaxbJsonProvider.class in the jersey config. Why priority is not working in my case? i bet I'm missing somethingGirovard
We know that adding JacksonJaxbJsonProvider can have some side effect? It's safe to add it?Girovard
B
10

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);
    }
}
Bootblack answered 3/8, 2017 at 8:19 Comment(4)
I am not sure that registering JacksonJaxbJsonProvider is appropriate workaround for me. I am afraid of side effects. I would like to find more targeted solution, just to register my mapper instead of default. Thanks.Heliogabalus
@KysilIvan Please see my answer again. It has been updated.Bootblack
I don't clearly understand consequences of JacksonJaxbJsonProvider registering, don't want to brake something, as application is big and not new. Thanks for your response, it is informative and helpful, though it is pity that there is no easier way to address this issue.Heliogabalus
@Bootblack We know that adding JacksonJaxbJsonProvider can have some side effect? It's safe to add it?Girovard
H
5

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.

Heliogabalus answered 3/8, 2017 at 10:51 Comment(7)
It's a fancy solution!Bootblack
Which version of jersey ?Laryngoscope
@Brice Jersey 2.xBootblack
yes but 2.21, 2.22, ... ? I don't see any documentation on Jersey on that, and as far as I know this isn't there yet, see jersey/jersey#2994Laryngoscope
I have jackson-core v. 2.8.6Heliogabalus
Adding the Priotiry(1) is not working for me. The only solution is to add the JacksonJaxbJsonProvider.class in the jersey config. Why priority is not working in my case? i bet I'm missing somethingGirovard
We know that adding JacksonJaxbJsonProvider can have some side effect? It's safe to add it?Girovard

© 2022 - 2024 — McMap. All rights reserved.