Spring Boot 2.5.0 Jersey JSON Deserialization not using custom objectmapper configuration

304 Views Asked by At

I'm trying to set up a Kotlin Spring Boot application that uses Jersey to make REST calls to DropWizard API applications from the Services/Repository layer. In the API layer the controllers use Spring MVC classes and annotations. So i've got Jersey set up as a filter that passes along 404's to Spring MVC which results in an application that is able to handle both type of requests.

I have abstracted an ApiUtils class where the JAX-RS calls are constructed, executed and the response is handled. This ApiUtils class is an @Component and has an @Autowired constructor to initialize it.

public ApiUtils(Client jerseyClient, ObjectMapper mapper, ServiceNameProvider serviceNameProvider) {
    this.client = jerseyClient;
//  this.client = ClientBuilder.newClient();
    this.mapper = mapper;
    this.serviceName = serviceNameProvider.getServiceName();

    //in case a jersey client is inserted with a ClientBuilder.newClient() no object mapper
    //is registered; to avoid fall back to default register the given mapper
    if (this.client != null
        && this.client.getConfiguration() != null
        && !this.client.getConfiguration().isRegistered(JacksonJsonProvider.class)
        && !this.client.getConfiguration().isRegistered(JacksonFeature.class)
    ) {
        this.client.register(new JacksonJsonProvider(this.mapper));
    }
}

Now i discovered that even though the primary custom objectmapper configured in Spring Boot is autowired into this ApiUtils class, the custom objectmapper configuration is not being applied. Even when i create a new client and register the objectmapper i experience the same result. I printed all settings of the custom objectmapper in the APIUtils class and they are exactly as configured.

When i add Annotations to the class that needs to be deserialized it succeeds, but without the annotation using the custom objectmapper it fails.

A good example is the DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES it is set to false but deserialization fails when there are unkown properties. As soon as i add the @JsonIgnoreProperties(ignoreUnknown = true) to the class itself it succeeds.

I've seen the custom objectmapper get registered in the autoconfiguration class of Jersey and Jackson when starting the application, i've seen the custom objectmapper being autowired in my ApiUtils class. But the errors that i get clearly indicate that a default objectmapper without clustomization is used.

This is my current config in Spring Boot to Customize the Objectmapper. But i have tried all builders and other types of configs i could find searching for the correct setup.

MyCustomObjectMapper

@Configuration
@Primary
class MyCustomObjectMapper : ObjectMapper() {

    init {

    val kotlinModule: KotlinModule = KotlinModule.Builder()
        .configure(KotlinFeature.NullIsSameAsDefault, true)
        .configure(KotlinFeature.StrictNullChecks, true)
        .configure(KotlinFeature.NullToEmptyCollection, true)
        .configure(KotlinFeature.NullToEmptyMap, true)
        .configure(KotlinFeature.SingletonSupport, true)
        .build()

    this.registerModules(
        kotlinModule,
        JavaTimeModule(),
        Jdk8Module(),
    )

    ...

    // deserialization configuration
    this.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
    ...
   }
}

JacksonConfig

@Provider
class JacksonConfig : ContextResolver<ObjectMapper> {

    @Autowired
    private lateinit var objectMapper: MyCustomObjectMapper

    override fun getContext(type: Class<*>?): ObjectMapper {
        return objectMapper
    }
}

JerseyConfig

@Configuration
@ApplicationPath(JERSEY_API_PATH)
class JerseyConfig : ResourceConfig() {

  init {
        
    register(packages("eu.mypackage.services.web.cg.configuration"));
    register(packages("eu.mypackage.services.web.cg.controller"));
    register(EndpointJerseyLoggingListener(JERSEY_API_PATH));

    //https://docs.spring.io/spring-boot/docs/2.0.x/reference/html/howto-jersey.htmls
    properties = mapOf(
        ServerProperties.RESPONSE_SET_STATUS_OVER_SEND_ERROR to true,
        ServletProperties.FILTER_FORWARD_ON_404 to true
    )

  }
}

I'm quite out of options... Is Spring MVC causing this issue, although i do not use it in the ApiUtils class? Is there another way to use a customized objectmapper for the Jersey Client and then autowire it to a external java ApiUtils class?

1

There are 1 best solutions below

0
Slice of Code On

The issue seemed to be that the Jersey client that was configured in my project was not correctly setup. This in combination with scanning of register(packages("eu.mypackage.services.web.cg.configuration")); that contained the

@Provider
class JacksonConfig : ContextResolver<ObjectMapper>

Seemed to cause my ApiUtils class to autowire a Jersey client that did not use the customized objectmapper.

So by annotating the custom objectmapper class with

@Configuration
@Primary

removing the

@Provider
class JacksonConfig : ContextResolver<ObjectMapper>

completely and creating a JerseyClientConfiguration class:

@Configuration
class JerseyClientConfiguration {

    @Value("\${jerseyClient.timeout}")
    var timeout: Long = 0

    @Value("\${jerseyClient.connectionTimeout}")
    var connectionTimeout: Long = 0

    @Value("\${jerseyClient.threads}")
    var threads = 0

    /**
     * Create a new instance of the JerseyClient and bind it to the Spring context.
     * Needed for ApiUtils
     */
    @Bean
    fun jerseyClient(): Client {

        val connectionManager = PoolingHttpClientConnectionManager()
        connectionManager.maxTotal = threads
        connectionManager.defaultMaxPerRoute = threads

        val client = ClientBuilder.newBuilder()
            .connectTimeout(connectionTimeout, TimeUnit.MILLISECONDS)
            .readTimeout(timeout, TimeUnit.MILLISECONDS)
            .property("jersey.config.client.async.threadPoolSize", threads)
            .build()

            val provider: JacksonJsonProvider = JacksonJaxbJsonProvider(
                ObjectMapper(),
                JacksonJaxbJsonProvider.DEFAULT_ANNOTATIONS
            )
        client.register(provider)
        return client
    }
}

I finally got the JerseyClient to use the custom configuration.

Maybe there is a better Spring Boot way to get the same config working. If so feel free to enlighten us.