4

I wanted to use a configured version of Jackson ObjectMapper in my project (ignoring null values and snake_case, also using some custom modules).

In my large project I wasn't able to get Spring MVC to actually use this mapper.

The build.gradle:

buildscript {
  ext {
    springBootVersion = '1.5.6.RELEASE'
  }
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
  }
}

apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'

version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
  mavenCentral()
}

dependencies {
  compile('org.springframework.boot:spring-boot-starter')
  compile("org.springframework.boot:spring-boot-starter-jetty:${springBootVersion}")
  compile("org.springframework.boot:spring-boot-starter-web:${springBootVersion}")

  compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.8.8'
  compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.8.8'

  testCompile('org.springframework.boot:spring-boot-starter-test')
}

My application.yml:

spring:
  application:
  name: Jackson test
jackson:
  property-naming-strategy: SNAKE_CASE
  default-property-inclusion: non_empty
debug: true

A container class:

public class MyLocationEntity {
    public String nameAndSnake;
}

A config class:

@Configuration
@EnableWebMvc
public class AppConfig {
}

And a controller:

@RestController
@RequestMapping("/test")
public class TestController {

  @Autowired
  private ObjectMapper objectMapper;

  @RequestMapping(value = "/test", produces = "application/json")
  public MyLocationEntity test() throws JsonProcessingException {
    MyLocationEntity location = new MyLocationEntity();
    location.nameAndSnake = "hello world";
    String expexted = objectMapper.writeValueAsString(location);
    return location;
  }
}

If I now look at the value of expected in the debugger it is {"name_and_snake":"hello world"}. But if I let the controller run through the actual response is {"nameAndSnake":"hello world"}.

When I remove @EnableWebMvc it works. How can I use the configured mapper with MVC and not remove the rest of the autoconfiguration for Web MVC?

M. Justin
  • 14,487
  • 7
  • 91
  • 130
Tom
  • 3,807
  • 4
  • 33
  • 58
  • `@EnableWebMvc` disables the web auto configuration of Spring Boot. Which configured mapper are you talking about? What isn't working with the auto configured instance? – M. Deinum Aug 17 '17 at 11:52
  • 1
    Why do you use EnableWebMvc? – JB Nizet Aug 17 '17 at 11:52
  • @JBNizet this is just the demo project to show the error. In this project it doesn't do anything. In the larger project it is needed. – Tom Aug 17 '17 at 12:02
  • That doesn't answer my question at all. Why is it needed in the larger project? EnableWebMvc **disables** web mvc autoconfiguration. It doesn't enable it. Read the documentation: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-spring-mvc-auto-configuration – JB Nizet Aug 17 '17 at 12:06
  • Ok I see. So basically if I `@EnableWebMvc` I disable the autoconfiguration and therefore my configuration done in the application.yml is not applied. <-- this would have been an answer and not "why do you want to use it" – Tom Aug 17 '17 at 12:10
  • 1
    M. Deinum did say almost exactly that, and you didn't believe him... But anyway, at least it made you read the documentation. – JB Nizet Aug 17 '17 at 12:16
  • See https://stackoverflow.com/a/30800851/143918 – vivo Jan 30 '18 at 08:44
  • Was the `jackson` property indented in your actual code, but not in the question due to a copy/paste error? Since other issues aside, the example code is currently mistakenly configuring the `jackson.property-naming-strategy` property rather than the correct `spring.jackson.property-naming-strategy` property (likewise for `default-property-inclusion`). – M. Justin Feb 10 '21 at 20:28

1 Answers1

9

It's not evident just from the Javadocs, but @EnableWebMvc disables the Spring Boot default web MVC auto configuration provided by WebMvcAutoConfiguration, including the use of the Jackson ObjectMapper bean configured by the application.yml properties. Per the Spring Boot Reference Documentation:

9.4.7. Switch off the Default MVC Configuration

The easiest way to take complete control over MVC configuration is to provide your own @Configuration with the @EnableWebMvc annotation. Doing so leaves all MVC configuration in your hands.

Since @EnableWebMvc has the (likely surprising) behavior of disabling auto configuration, using this annotation may have unintended and undesirable side-effects. A different approach may be more appropriate.

That being said, it is possible that the behavior of @EnableWebMvc is still desired. To use application.yml properties in concert with the @EnableWebMvc annotation, the MVC configuration must be manually configured to mimic the relevant disabled Spring Boot auto-configuration. There are a few different possible approaches to this.

The first approach is to duplicate the Spring Boot configuration code from WebMvcAutoConfiguration.EnableWebMvcConfiguration.configureMessageConverters(). This will replace the message converters — including the MappingJackson2HttpMessageConverter containing the unconfigured ObjectMapper — with the ones that would have used with the default Spring Boot configuration:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private ObjectProvider<HttpMessageConverters> messageConvertersProvider;

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        this.messageConvertersProvider
                .ifAvailable((customConverters) -> converters.addAll(customConverters.getConverters()));
    }
}

Alternatively, rather than using the default Spring Boot list of message converters, it is possible to swap in just the ObjectMapper or MappingJackson2HttpMessageConverter bean provided by Spring Boot (which have the application.yml properties applied):

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.stream()
                .filter(c -> c instanceof MappingJackson2HttpMessageConverter)
                .map(c -> (MappingJackson2HttpMessageConverter) c)
                .forEach(c -> c.setObjectMapper(objectMapper));

    }
}

or

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter;

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        for (int i = 0; i < converters.size(); i++) {
            if (converters.get(i) instanceof MappingJackson2HttpMessageConverter) {
                converters.set(i, mappingJackson2HttpMessageConverter);
            }
        }
    }
}
M. Justin
  • 14,487
  • 7
  • 91
  • 130