spring-hateoas: @EnableHypermediaSupport is not compatible with Spring Boot's Jackson2ObjectMapperBuilder

I am trying to customize Jackson serialization for ISO dates. Per Spring Boot instructions, I created a @Bean of type Jackson2ObjectMapperBuilder:

    @Bean
    public Jackson2ObjectMapperBuilder objectMapperBuilder() {
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
        builder.featuresToDisable(
                SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
                DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
        return builder;
    }

However, I find that these settings are not applied when using @EnableHypermediaSupport. When I remove the annotation, I see the effects of the Jackson serialization settings.

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Comments: 49 (11 by maintainers)

Most upvoted comments

To take the comment from @gregturn a bit further, I agree that the best solution is to work off of Spring Boot’s auto-configuration instead of creating your own custom ObjectMapper. However, in your code, it doesn’t seem that you are addressing the issue of using @EnableHypermediaSupport. For most cases (i.e. serializing) this doesn’t seem to be needed, and putting all settings for Jackson in application.yml etc. works perfectly fine. On the other hand, in the case where you want to deserialize json responses, we need to use the _halObjectMapper in Spring HATEOAS to get this to work properly.

Here is an example of a very simple solution which worked for me (using Spring Boot 1.5.2 and Spring HATEOAS 0.23.0, and it is also using Spring Boot’s own API for customizing the auto-configured ObjectMapper (just like @gregturn shows in his code):

  • In Application.java: @EnableHypermediaSupport(type = HypermediaType.HAL)

  • In application.yml:

spring:
      jackson:
          date-format: com.fasterxml.jackson.databind.util.ISO8601DateFormat
  • Any @Configuration-annotated class:
@Autowired
private ObjectMapper _halObjectMapper;

@Bean
public Jackson2ObjectMapperBuilderCustomizer objectMapperBuilder() {
    return builder -> builder.configure(_halObjectMapper);
}

It’s clean and simple, and not very invasive either.

To expand on @thebignet’s solution you can apply the default configuration from Boot’s Jackson2ObjectMapperBuilder by injecting it and using its configure method.

@Configuration
public class ObjectMapperCustomizer {
  private static final String SPRING_HATEOAS_OBJECT_MAPPER = "_halObjectMapper";

  @Autowired
  @Qualifier(SPRING_HATEOAS_OBJECT_MAPPER)
  private ObjectMapper springHateoasObjectMapper;

  @Autowired
  private Jackson2ObjectMapperBuilder springBootObjectMapperBuilder;

  @Primary
  @Bean(name = "objectMapper")
  ObjectMapper objectMapper() {
    this.springBootObjectMapperBuilder.configure(this.springHateoasObjectMapper);

    return springHateoasObjectMapper;
  }
}

Update

Section 27.1.8 - Spring HATEOAS in the Spring Boot docs indicates that simply leaving off the @EnableHypermediaSupport annotation will let Boot’s autoconfiguration kick in for HATEOAS and create a proper ObjectMapper with the standard modules loaded and properties and features applied.

Are you using HAL? If so, we found that Spring HATEOAS constructs its own object mapper. In order to address this in our project, we did the following to get the whole application using the same configuration.

    private static final String SPRING_HATEOAS_OBJECT_MAPPER = "_halObjectMapper";

    @Autowired
    @Qualifier(SPRING_HATEOAS_OBJECT_MAPPER)
    private ObjectMapper springHateoasObjectMapper;

    @Bean(name = "objectMapper")
    ObjectMapper objectMapper() {
        springHateoasObjectMapper.configure(SerializationFeature.INDENT_OUTPUT, true);
        return springHateoasObjectMapper;
    }

Resolved via #719 and #723.

You are right. Its more the other way round (Spring Data REST is using HATEOAS). So this make no sense. Anyway to provide a third workaround, I use a BeanPostProcessor to configure my ObjectMapper:

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.zalando.jackson.datatype.money.MoneyModule;

public class ObjectMapperCustomizer implements BeanPostProcessor {

    /*
    * (non-Javadoc)
    * @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization(java.lang.Object, java.lang.String)
    */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        if (!(bean instanceof ObjectMapper)) {
            return bean;
        }

        ObjectMapper mapper = (ObjectMapper) bean;
        mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        mapper.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false);
        mapper.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false);
        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        mapper.registerModules(new MoneyModule(), new JavaTimeModule());

        return mapper;
    }

    /*
    * (non-Javadoc)
    * @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessBeforeInitialization(java.lang.Object, java.lang.String)
    */
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
}

Would be nice to have this one documented as a “gotcha” in the docs.

I had a similar issue with spring boot and HATEOAS. The workaround was to register a bean post processor, rather than creating multiple beans of the same type.

@Service
public class ObjectMapperBeanPostProcessor implements BeanPostProcessor {
	private static final String HAL_OBJ_MAPPER_BEAN = "_halObjectMapper";

	@Nullable
	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

		if (HAL_OBJ_MAPPER_BEAN.equals(beanName)) {
			// pass bean back through Jackson mapper to re-configure.
			new Jackson2ObjectMapperBuilder().configure((ObjectMapper) bean);
		}
		return bean;
	}
}

I just updated to Spring Boot 2.0.4 and Spring HATEOAS 0.25.0 which lead to a failing Jackson configuration as _halObjectMapper is no longer available.

I was trying to refactor my code according to the changes in #719, but to no avail.

With 0.25.0, what would be a simple way to configure the ObjectMapper that is being used by Spring HATEOAS?

I’ve sifted through all the comments, and have an example github repo (Spring Boot 1.5.4 + Spring HATEOS 0.23) that covers this in detail (https://github.com/gregturn/spring-hateoas-customize-jackson).

If you read the Spring Boot docs, you have multiple options:

  • Set various properties (like spring.jackson.serialization.indent-output=true). You can uncomment what I have in src/main/resources/application.properties, and see that by starting up my app, this works.
  • Register a Jackson2ObjectMapperBuilderCustomizer bean, which is handed a copy of Boot’s autoconfigured Jackson2ObjectMapperBuilder after applying Boot’s defaults. If you uncomment the @Bean annotation inside my repo’s CustomizeJackson configuration class, you can see this behavior applied.

Both of these solutions appear simpler to plug in your custom ObjectMapper settings than the other presented mechanisms.

It’s also possible to create your own Jackson2ObjectMapperBuilder, but there are actually a handful of other things you must undertake should you take this approach. As stated in the docs,

If you want to replace the default ObjectMapper completely, either define a @Bean of that type and mark it as @Primary, or, if you prefer the builder-based approach, define a Jackson2ObjectMapperBuilder @Bean. Note that in either case this will disable all autoconfiguration of the ObjectMapper.

Notice the last sentence in that quote: “You will disable all autoconfiguration of the ObjectMapper”. That’s why this is NOT recommended.

I haven’t reconciled this with Spring Data REST’s HAL-based object mapper (yet). But I wanted to clarify the proper approach.

P.S. This probably would make good material for Spring HATEOAS’s reference docs.

None of the workarounds in this ticket seem to be working any longer with spring-hateoas 0.23 and boot 1.5.3.

@Slf4j
@Configuration
public class JacksonConfig
{
    private static final String SPRING_HATEOAS_OBJECT_MAPPER = "_halObjectMapper";

    @Autowired
    @Qualifier(SPRING_HATEOAS_OBJECT_MAPPER)
    private ObjectMapper springHateoasObjectMapper;

    @Autowired
    private Jackson2ObjectMapperBuilder springBootObjectMapperBuilder;

    @Bean(name = "objectMapper")
    @Primary
    ObjectMapper objectMapper()
    {
        this.springBootObjectMapperBuilder.configure(this.springHateoasObjectMapper);
        log.info("HATEOAS Jackson ObjectMapper configured1");
        return springHateoasObjectMapper;
    }
}

The objectMapper method is never called (verified with debugger).

@pleimann thanx for the suggested workaround, it works. I would also suggest to declare the @Bean(name = "objectMapper") as @Primary; this is how Spring Boot’s autoconf works, see JacksonAutoConfiguration

If it’s any help, I ended up mixing @gpaul-idexx and @thomasletsch solutions

@Configuration
public class ObjectMapperCustomizer {

  private static final String SPRING_HATEOAS_OBJECT_MAPPER = "_halObjectMapper";

  @Autowired
  @Qualifier(SPRING_HATEOAS_OBJECT_MAPPER)
  private ObjectMapper springHateoasObjectMapper;

  @Bean(name = "objectMapper")
  ObjectMapper objectMapper() {
    springHateoasObjectMapper.configure(SerializationFeature.INDENT_OUTPUT, true);
    springHateoasObjectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    springHateoasObjectMapper.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false);
    springHateoasObjectMapper.configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false);
    springHateoasObjectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    springHateoasObjectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
    springHateoasObjectMapper.registerModules(new JavaTimeModule());
    return springHateoasObjectMapper;
  }

}

This might be a more Spring-Boot approach to rendering Java8 dates with the HAL Jackson Object Mapper.

I’m also loosing the Java 8 com.fasterxml.jackson.datatype.jsr310.JavaTimeModule if using @EnableHypermediaSupport with spring boot 1.3.1 Thanks to @thomasletsch for the BeanPostProcessor workaround!

I believe this is because Spring HATEOAS uses its own object mapper. You will have to set these flags on that instance. You can access the object mapper using the bean factory:

private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper";

@Autowired
private BeanFactory beanFactory

...

@Bean
public ObjectMapper objectMapper() {
    ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME);
    //set your flags

    return halObjectMapper
}