Problem:

I want to define my own ObjectMapper as a bean (as it needs to be injected with other dependencies) without suddenly breaking the rest of my application.

  @Bean("my-object-mapper")
  @Qualifier("my-object-mapper") 
  public ObjectMapper flowObjectMapper(RequiredDependencies etc) {  ...  }

This unfortunately breaks the rest of the application. Despite giving it a name that nobody else uses, a Qualifier that nobody else knows about, things like Feign, RabbitListeners, etc. suddenly start using this completely differently configured ObjectMapper as their own. Why? Because JacksonAutoConfiguration decides not to configure the default primary bean because ConditionalOnMissingBean thinks that a bean with the correct type (not name, not qualifier) should match. This results in some completely randomly configured bean that was clearly not intended to be used for anything but a specific purpose (hence the name and qualifier) is suddenly used as the default globally for everything.

Would it not have made more sense to still create this Primary bean if there is no suitable candidate which is unqualified and/or has the correct bean name?

Work-arounds:

  • Define your own Primary ObjectMapper. Problem: how do I know how the Spring default one was configured? I suppose I could copy the source code if that doesn't change too often... builder.createXmlMapper(false).build() -- poor solution IMHO.

  • Subclass ObjectMapper so Spring's type matching doesn't mistake my custom mapper as something it can use... won't work most likely, as it is still an ObjectMaper... so wrap it then and delegate... ouch.

  • Complicated stuff to register my bean as a non-primary bean using a BeanFactoryPostProcessor. Seriously?

Possible solutions that would work for me:

  • Allow a non-primary bean to be defined easily. @Bean(primary = false) or @Bean(cannotBeUsedUnqualified = true)

  • Introduce a ConditionalOnMissingUnqualifiedBean and use that in JacksonAutoConfiguration. Probably not backwards compatible.

Comment From: wilkinsona

Thanks for the suggestions.

Generally speaking, if an instance of something that is widely used is intended to be used for a specific purpose, particularly if it's in a single place, it may be better not to define it as a bean. I can't tell if that applies here as three-line code snippet above doesn't provide us with enough information.

Alternatively if you do need to define your ObjectMapper as a bean, there are a couple of other options that you could consider:

  1. You could define a bean of a custom type that provides access to the specific-purpose ObjectMapper. You can then inject this "holder" bean and retrieve the ObjectMapper from it.
  2. You can create your own auto-configuration, registered via spring.factories and @AutoConfigureAfter(JacksonAutoConfiguration.class) to define any number of additional ObjectMapper beans that your application requires. Due to @AutoConfigureAfter(JacksonAutoConfiguration.class), Spring Boot's auto-configured ObjectMapper will not back off as it will be defined before any of your ObjectMapper beans have been defined.

Comment From: hjohn

Thanks for your response!

I do need this mapper in multiple locations, and the mapper itself also needs to be configured with other discoverable beans. Normally, I would just create a static mapper, make it public final and use it like that. However, as I need a list of spring beans which controls the behavior of one of the mapper's modules, I cannot make it static.

Your option #1 is similar to one of the options I suggested, and is basically wrapping the class in a different type so it isn't an injection candidate. Although it certainly works as a work-around, I feel there should be a better way to achieve this.

Option #2 is interesting, and seems like a decent solution, I'm gonna test this out soon.

For now I've just copied the code from JacksonAutoConfiguration to create the primary object mapper.

Comment From: RubenGamarrarodriguez-tomtom

Would it help to create a MyApplicationCustomObjectMapperSupplier or similarly named bean?

That way one could reuse that bean when needed through the user MyApplication code. Depending on the scenario it could even make easier to understand the application.

In the direction of the Principle of Least Surprise if I see that there is an ObjectMapper I assume that there is only one and works fine with things like you listed (Feign, RabbitListeners) but if I see an MyApplicationCustomObjectMapperSupplier bean I would suspect that there is a reason for it and understand that there is a reason why simply providing a simple ObjectMapper would not suffice for this application (regardless of the features or limitation of the dependency injection framework used). The using the context from Spring core for dependency injection should be a detail of the application.

Using a spring factory would personally also make easier to see this need for another ObjectMapper in a non surprising approach.

On the other side I also see having to create a bean that provides this custom crafted object mapped smells a bit.

Nevertheless as listed in other comments there are, indeed, different options to overcome this bug/behaviour/feature.

Comment From: hjohn

Although I appreciate the solution, I'm well aware it is possible to create factories, or wrappers, to solve this issue. But why should I have to? My classes want to use proper DI as well and just because I want to inject a class that happens to be part of auto configuration should not really affect that. There's probably hundreds of those classes, and maybe I have even been "configuring" some of these without even realizing that some auto configured component is now working subtly different than the default. ObjectMapper is however definitely one of the most visible.

I feel this is a general problem with auto configuration. The least surprising would be that my feign clients and rabbit consumers and rest endpoints wouldn't suddenly start behaving differently when I have need of a Bean that happens to be of the same class that is conditionally created in Spring Boot auto configuration -- I really don't keep track of all beans created, so it is kind of surprising when it affects other parts of Spring:

1) I have a working application 2) I define an ObjectMapper for some obscure case, clearly tagging it for that case only 3) Test all over the application in (unrelated) code break suddenly or worse nothing breaks until you notice subtle issues in production (because you only configured a slight date formatting change in the obscure ObjectMapper)

In non-boot Spring, we'd get an error during context creation that there are duplicate beans. Certainly not ideal either, but at least you're aware you're gonna have to open the Qualifier toolbox to support both beans and make a conscious choice about it.

Sharing ObjectMapper's everywhere in an application is already a very questionable practice in my opinion, and this certainly doesn't help.

Comment From: snicoll

@hjohn registering the bean as not a candidate for autowiring should do what you want once https://github.com/spring-projects/spring-boot/issues/41526 is resolved. You've also requested https://github.com/spring-projects/spring-framework/issues/26528 that will have a direct impact on those conditions.

I don't know if we have an issue for reviewing the latter. We could use this issue if we don't.

Comment From: wilkinsona

We discussed this today and think that we can use this issue to add support for the bean-related conditions ignoring beans that are not default candidates.

Comment From: wallind

Problem:

I want to define my own ObjectMapper as a bean (as it needs to be injected with other dependencies) without suddenly breaking the rest of my application.

Just posting because I don't see it otherwise stated in simple terms, if this^ exactly describes your issue and you don't have the energy to read through all the linked resources, your answer is this:

Amend your bean declarations to set defaultCandidate = false, something like this:

  @Bean(defaultCandidate = false)
  @Qualifier("YOUR_QUALIFIER")
  fun custombjectMapper(): ObjectMapper {  // ...  }

Not sure exactly what version this functionality was incorporated in, but available for me on 3.4.2.

Comment From: hjohn

It was added because of this post amongst others. It maybe new in Spring Boot 3.4.2, but it was added in Spring in version 6.2