13

I have a spring-boot application that exposes a json REST API. For mapping objects to json it uses the built-in jackson ObjectMapper configured by spring-boot.

Now I need to read some data from a yaml file and I found that an easy way to do it is using Jackson - for this I need to declare a different ObjectMapper to convert yaml to objects. I declared this new mapper bean with a specific name to be able to inject it in my service dealing with reading from the yaml file:

@Bean(YAML_OBJECT_MAPPER_BEAN_ID)
public ObjectMapper yamlObjectMapper() {
    return new ObjectMapper(new YAMLFactory());
}

But I need a way to tell all the other "clients" of the original json ObjectMapper to keep using that bean. So basically I would need a @Primary annotation on the original bean. Is there a way to achieve this without having to redeclare the original ObjectMapper in my own configuration (I'd have to dig through spring-boot code to find and copy its configuration)?

One solution I found is to declare a FactoryBean for ObjectMapper and make it return the already declared bean, as suggested in this answer. I found by debugging that my original bean is called "_halObjectMapper", so my factoryBean will search for this bean and return it:

public class ObjectMapperFactory implements FactoryBean<ObjectMapper> {

    ListableBeanFactory beanFactory;

    public ObjectMapper getObject() {
        return beanFactory.getBean("_halObjectMapper", ObjectMapper.class);
    }
    ...
}

Then in my Configuration class I declare it as a @Primary bean to make sure it's the first choice for autowiring:

@Primary
@Bean
public ObjectMapperFactory objectMapperFactory(ListableBeanFactory beanFactory) {
    return new ObjectMapperFactory(beanFactory);
}

Still, I'm not 100% happy with this solution because it relies on the name of the bean which is not under my control, and it also seems like a hack. Is there a cleaner solution?

Thanks!

luboskrnac
  • 23,973
  • 10
  • 81
  • 92
Timi
  • 774
  • 9
  • 16
  • I also tried to use `YAMLMapper` (which extends `ObjectMapper`) as my custom bean type, but spring would still complain that it finds two beans of type `ObjectMapper`. I didn't expect this, although in a way it's plausible. So again I learned something new today. It means that if I autowire a field of type `List` I'll obtain all the available beans in the context... – Timi May 26 '17 at 09:03
  • check out the update to my answer.. – Darshan Mehta May 26 '17 at 09:44

4 Answers4

5

You can define two ObjectMapper beans and declare one as primary, e.g.:

@Bean("Your_id")
public ObjectMapper yamlObjectMapper() {
    return new ObjectMapper(new YAMLFactory());
}

@Bean
@Primary
public ObjectMapper objectMapper() {
    return new ObjectMapper();
}

Once done, you can use your objectmapper bean with @Qualifier annotation, e.g.:

@Autowired
@Qualifier("Your_id")
private ObjectMapper yamlMapper;

Update

You can dynamically add your ObjectMapper to Spring's bean factory at runtime, e.g.:

@Configuration
public class ObjectMapperConfig {

    @Autowired
    private ConfigurableApplicationContext  context;

    @PostConstruct
    private void init(){
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ObjectMapper.class);
        builder.addConstructorArgValue(new JsonFactory());
        DefaultListableBeanFactory factory = (DefaultListableBeanFactory) context.getBeanFactory();
        factory.registerBeanDefinition("yamlMapper", builder.getBeanDefinition());
        Map<String, ObjectMapper> beans = context.getBeansOfType(ObjectMapper.class);
        beans.entrySet().forEach(System.out::println);
    }
}

The above code adds a new bean into context without changing the existing bean (sysout prints two beans in the end of init method). You can then use "yamlMapper" as qualifier to autowire it anywhere.

Update 2 (from question author):

The solution suggested in 'Update' works and here's a simplified version:

@Autowired
private DefaultListableBeanFactory beanFactory;

@PostConstruct
private void init(){
    BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(YAMLMapper.class);
    beanFactory.registerBeanDefinition("yamlMapper", builder.getBeanDefinition());
}
Timi
  • 774
  • 9
  • 16
Darshan Mehta
  • 30,102
  • 11
  • 68
  • 102
  • This was the first thing I tried, but `new ObjectMapper()` is not configured in the same way as the one defined by spring-boot, so I got a lot of failing tests. That's why I'd like to keep using exactly the same bean, not to affect the current API. – Timi May 26 '17 at 08:34
  • This is exactly the same answer than I created before. Why are you doing it? – luboskrnac May 26 '17 at 08:55
  • @luboskrnac there's less than 2 minute difference between your and my answer. Do you think I read your answer, rephrased it and typed my answer with explanation in that time? – Darshan Mehta May 26 '17 at 09:13
  • actually 5 minutes – luboskrnac May 26 '17 at 09:15
  • `7:50:01` and `7:51:56`, 5 mins? – Darshan Mehta May 26 '17 at 09:44
  • I believe with your latest suggestion I would still get an exception at startup that two different beans exist for the same type and spring cannot decide which one to inject (I mean for the existing beans that use the original ObjectMapper and don't have a qualifier). I haven't tried it, though. – Timi May 29 '17 at 05:12
  • @Timi I tried it before posting and it didn't throw an Exception. – Darshan Mehta May 29 '17 at 08:54
  • You're right, it works (I need to dig deeper to understand why)! Thanks for your help! I simplified it slightly but don't know how to add the code snippet in the comment (sorry, I'm very new). – Timi May 29 '17 at 10:42
  • @Timi Can someone please post a complete solution? After configuring as proposed in Update(2), I still get test failures to deserialization which tells me that the default spring boot object mapper is not in use. – Srki Rakic Feb 27 '21 at 21:04
  • @Srki Rakic, the complete solution is to add the content of Update or Update 2 to a Configuration class. Or you could try my answer at the bottom to redeclare the original bean as primary. – Timi Mar 02 '21 at 07:37
  • @Timi thanks for the clarification. Unfortunately, this seems to still override the default object mapper for me. – Srki Rakic Mar 03 '21 at 21:36
4

Other option is to wrap custom mapper into custom object:

@Component
public class YamlObjectMapper {
    private final ObjectMapper objectMapper;

    public YamlObjectMapper() {
        objectMapper = new ObjectMapper(new YAMLFactory());
    }

    public ObjectMapper getMapper() {
        return objectMapper;
    }
}

Unfortunately this approach requires calling getMapper after you inject YamlObjectMapper.

luboskrnac
  • 23,973
  • 10
  • 81
  • 92
2

I believe defining explicit primary object mapper for MVC layer should work this way:

 @Primary
 @Bean
 public ObjectMapper objectMapper() {
     return Jackson2ObjectMapperBuilder.json().build();
 }

All beans that autowire object mapper via type will use above bean. Your Yaml logic can autowire via YAML_OBJECT_MAPPER_BEAN_ID.

luboskrnac
  • 23,973
  • 10
  • 81
  • 92
  • I tried your suggestion, but it's the same as having `new ObjectMapper()` - it doesn't reuse the spring-boot configuration for the original mapper, so again, I get failing tests. – Timi May 26 '17 at 08:53
  • BTW, Darshan Mehta created answer that plagiarized mine in bad way, because this creates better bean initiated with Spring defaults. – luboskrnac May 26 '17 at 08:57
  • What kind of exception you get in tests? – luboskrnac May 26 '17 at 08:58
  • The tests are failing because the output json doesn't match the expected value when I use `new ObjectMapper()` instead of the original spring-boot instance. I suppose that one has some additional configuration related to how it formats certain values. – Timi May 29 '17 at 05:14
0

I just realized that I don't need to use a FactoryBean, I could just as well declare a regular bean as @Primary and make it return the original bean, like this:

@Bean
@Primary
public ObjectMapper objectMapper(@Qualifier("_halObjectMapper") ObjectMapper objectMapper) {
    return objectMapper;
}

This makes the configuration slightly cleaner, but still requires the exact name of the original ObjectMapper. I guess I'll stay with this solution, though.

Timi
  • 774
  • 9
  • 16