Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@EnableHypermediaSupport is not compatible with Spring Boot's Jackson2ObjectMapperBuilder #333

Closed
pimlottc opened this issue Apr 15, 2015 · 49 comments
Assignees

Comments

@pimlottc
Copy link

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.

@gpaul-idexx
Copy link

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;
    }

@vivin
Copy link

vivin commented Apr 15, 2015

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
}

@pimlottc
Copy link
Author

Thanks for the workaround, that works. However, Spring HATEOAS should really honor any settings configured via Jackson2ObjectMapperBuilder when constructing its own ObjectMapper.

Similarly, the HATEOAS ObjectMapper ignores spring.jackson.deserialization/spring.jackson.serialization environment properties.

@vivin
Copy link

vivin commented Apr 15, 2015

i think that is a side-effect of Spring HATEOAS having its own module and mapper to handle HAL. I ended up configuring the HAL mapper to recognize application/json and application/*+json as well since I have custom media-types that end up representing links using HAL.

@odrotbohm
Copy link
Member

Yes we use a custom ObjectMapper instance but we expose that as a Spring bean to benefit from all the defaulting Spring Boot applies. So if you see those defaults not applied, it's probably worth filing a ticket in Boot as it should apply the defaults to all ObjectMapper instances available in the ApplicationContext.

@odrotbohm odrotbohm self-assigned this May 21, 2015
@odrotbohm odrotbohm changed the title @EnableHypermediaSupport is not compatible with Jackson2ObjectMapperBuilder @EnableHypermediaSupport is not compatible with Spring Boot's Jackson2ObjectMapperBuilder May 21, 2015
@schvanhugten
Copy link

If you would like to have ISO dates with Jackson you can use the following annotation:

    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssX")
    private Date fromDate;

@thomasletsch
Copy link

From what I have understood so far, most ObjectMapper are defined in the RepositoryRestMvcConfiguration. There is even a "halObjectMapper" which would get all the adjustments from the RepositoryRestConfigurerAdapter. HATEOAS uses a bean named "_halObjectMapper" which is not using the RepositoryRestConfigurerAdapter settings :-)
Since the RepositoryRestConfigurerAdapter stuff is applied directly in the RepositoryRestMvcConfiguration, this class seems a good candidate for centralized object mapper creation. Don't know where spring boot is involved here...
Can't we just use the "halObjectMapper" from the RepositoryRestMvcConfiguration as default for HATEOAS?

@gregturn
Copy link
Contributor

RepositoryRestMvcConfiguration and RepositoryRestConfigurerAdapter are parts of Spring Data REST and not available in projects confined to Spring HATEOAS.

@thomasletsch
Copy link

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;
    }
}

@sdeleuze
Copy link

We had a related issue reported on Spring Framework side (SPR-13608). I would like to investigate and potentially propose a fix that avoid to have duplicated ObjectMapper instances, since it produces some difficult to understand behaviors like the one described in SPR-13608.

Not sure yet if the fix will be on Spring HATEOAS or Spring Boot side, buy I am going to investigate and send my feedback here.

@sdeleuze
Copy link

sdeleuze commented Nov 5, 2015

I had a look to what is related to ObjectMapper in Spring HATEOAS (I have removed some unrelated lines of code):

class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper";

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        if (types.contains(HypermediaType.HAL)) {

            if (JACKSON2_PRESENT) {
                BeanDefinitionBuilder halQueryMapperBuilder = rootBeanDefinition(ObjectMapper.class);
                registerSourcedBeanDefinition(halQueryMapperBuilder, metadata, registry, HAL_OBJECT_MAPPER_BEAN_NAME);

                BeanDefinitionBuilder customizerBeanDefinition = rootBeanDefinition(DefaultObjectMapperCustomizer.class);
                registerSourcedBeanDefinition(customizerBeanDefinition, metadata, registry);

                BeanDefinitionBuilder builder = rootBeanDefinition(Jackson2ModuleRegisteringBeanPostProcessor.class);
                registerSourcedBeanDefinition(builder, metadata, registry);
            }
    }

    private List<HttpMessageConverter<?>> potentiallyRegisterModule(List<HttpMessageConverter<?>> converters) {

            for (HttpMessageConverter<?> converter : converters) {
                if (converter instanceof MappingJackson2HttpMessageConverter) {
                    MappingJackson2HttpMessageConverter halConverterCandidate = (MappingJackson2HttpMessageConverter) converter;
                    ObjectMapper objectMapper = halConverterCandidate.getObjectMapper();
                    if (Jackson2HalModule.isAlreadyRegisteredIn(objectMapper)) {
                        return converters;
                    }
                }
            }

            ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
            MappingJackson2HttpMessageConverter halConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class);
            halConverter.setObjectMapper(halObjectMapper);

            List<HttpMessageConverter<?>> result = new ArrayList<HttpMessageConverter<?>>(converters.size());
            result.add(halConverter);
            result.addAll(converters);
            return result;
        }
}

On Spring Boot side, it detects ObjectMapper or Jackson2ObjectMapperBuilder @Bean and uses the resulting ObjectMapper to configure Spring MVC HttpMessageConverter. So Spring Boot is not applying a default configuration to existing ObjectMapper, but rather creates the ObjectMapper instance for other components (Spring MVC, Spring HATEOAS).

So what about making Spring Boot registering an ObjectMapper bean when Spring HATEOAS is detected in the classpath with a bean name that Spring HATEOAS will recognize and reuse? If there is a user provided one, we could use ObjectMapper.copy() to create a new instance and avoid that the customization done by Spring HATEOAS cause some side effect for other parts just using a "regular" ObjectMapper like Spring MVC?

Based on my HypermediaSupportBeanDefinitionRegistrar understanding, that would require some changes to make it using an existing "_halObjectMapper" bean if it exists? Or maybe we could use the "halObjectMapper" bean name that seems more suitable for such purpose? Spring Data REST already uses this bean name so we may pay attention to that I guess ...

Any thoughts @olivergierke @gregturn ?

@abhishek2bommakanti
Copy link

Has there been any progress on this issue? We've experienced this issue in other places in our application since I reported SPR-13608 and it's really making things difficult for us.

@khong07
Copy link

khong07 commented Dec 2, 2015

hi,

I'm facing almost the "same" problem. I don't use Spring Data Rest but only Spring Boot 1.3.0, Spring MVC and Spring Hateoas and i have my own ObjectMapper, named jacksonObjectMapper with @Primary.
I end up with two beans jacksonObjectMapper and _halObjectMapper.
I want to use hateoas to provide _links on my controllers but from Spring Boot 1.3.0, it doesn't worked anymore because the my own jacksonObjectMapper is not configured with Jackson2HalModule.

So i must do a workaround

  1. Or i named my jacksonObjectMapper as _halObjectMapper >_<
  2. Or I do exactly as HalObjectMapperConfiguration in Spring Boot 1.2.5

Do you have any plan for this issue?


!!! Updated !!!
OK, I know about my problem, why links do not display on output response. In fact, my class ToBeReturnedResource doesn't extends of ResourceSupport but contains Link or contains an inner class extends of ResourceSupport so TypeConstrainedMappingJackson2HttpMessageConverter cannot read it....

@scottresnik
Copy link

I am not able to use use Spring Boot in my environment. Only Spring HATEOAS and Spring Data REST. The only way I was able to globally format my dates was to follow the third work around provided by @thomasletsch .

@nschwalbe
Copy link

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!

@thebignet
Copy link

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.

@aristotelos
Copy link

The solution described by @thebignet doesn't work for me, because the Spring HATEOAS BeanPostProcessor is called after the added custom BeanPostProcessor. This cannot be influenced by the Ordered implementation because unordered BeanPostProcessor instances seem to be called before instances.

There seems to be an implementation attempt already at Spring Boot, which however does not work: [https://github.com/spring-projects/spring-boot/blob/v1.3.2.RELEASE/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfiguration.java] contains a class HalObjectMapperConfigurer that seems intended for the purpose of enabling the default settings described at [https://docs.spring.io/spring-boot/docs/current/reference/html/howto-spring-mvc.html#howto-customize-the-jackson-objectmapper]. However, this does not work!

@thebignet
Copy link

Actually, I didn't use a postProcessor, since I can't really trust order. Instead, I used an @Autowired dependency, which means that _halObjectMapper bean has to be created before my objectMapper bean. This objectMapper is the same as _halObjectMapper, but enhances it during its creation.

@aristotelos, try to use a dependant @bean declaration instead of using a PostProcessor (see my example) and see if that works for you.

@aristotelos
Copy link

@thebignet You're right, I tested your solution and it works! The fault was on my side.

At my first try of your solution it didn't seem to work as well. My use case was enabling the DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, and testing with an unknown property. After some debugging I found out that this feature was set correctly using your code, but using the @JsonUnwrapped annotation on a property in my class caused unknown properties to be ignored without failure... I searched and there already exists an issue for this: [https://github.com/FasterXML/jackson-databind/issues/650]. Sigh...

@rocketraman
Copy link

I have this problem too... Jackson annotations like @JsonProperty on my classes are ignored when using Spring Boot with autoconfigured HATEOAS. The workaround for @thebignet does re-enable the appropriate Jackson annotation processing, as long as @EnableHypermediaSupport is not set (which disables the Spring Boot autoconfig).

I find it interesting that if @EnableHypermediaSupport is then added to the application, the Spring boot auto-configuration is turned off, and the same behavior of the Jackson annotations being ignored is observed. Does this mean the issue is in Spring HATEOAS?

This issue is marked as waiting-for-feedback. From who?

@sebster
Copy link

sebster commented Apr 20, 2016

I had the same issues, spent quite some time to figure out why "spring.jackson.serialization.indent_output=true" in my application.properties was not being applied. Turned out that turning on/off the @EnableHypermediaSupport annotation made the difference. I'm also hoping this can be fixed, because it's quite confusing, hard to google, and hard to debug.

@pleimann
Copy link

pleimann commented May 6, 2016

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.

@elnur
Copy link

elnur commented May 28, 2016

Can't upgrade from Boot 1.2 to 1.3 because of this. Is the issue gonna be fixed?

@gregturn
Copy link
Contributor

gregturn commented May 10, 2017

If you're trying to get your hands on the ObjectMapper used for HAL, and already asking the app context for that bean, this is a crude option:

https://github.com/spring-projects/spring-hateoas/blob/master/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java#L83

Be advised we're aware of this issue and plan to tackle it in the near future, after wrapping up work on the Affordance API (https://github.com/spring-projects/spring-hateoas/tree/rebase/affordances).

@Ramblurr
Copy link

@gregturn Yes, see in my snippet I am attempting to get a handle on the _halObjectMapper bean, however it isn't working. My objectMapper() bean method is never called, I think because the autowired deps aren't fulfilled.

@Ramblurr
Copy link

Ramblurr commented May 10, 2017

I can't trigger the default spring boot autoconfiguration:


   HypermediaAutoConfiguration.HypermediaConfiguration:
      Did not match:
         - @ConditionalOnMissingBean (types: org.springframework.hateoas.LinkDiscoverers; SearchStrategy: all) found bean 'org.springframework.hateoas.LinkDiscoverers#0' (OnBeanCondition)
      Matched:
         - @ConditionalOnClass found required class 'com.fasterxml.jackson.databind.ObjectMapper'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)

I am not using the @EnableHypermediaSupport annotation. Where is that LinkDiscoverers bean coming from?

Edit: It's combing from the RepositoryRestMvcConfiguration in spring data rest.

@pcornelissen
Copy link

pcornelissen commented May 22, 2017

Arrrgghhh, this cost me a few precious hours. I could not find why I wasn't able to get the LocalDateTime to be "jsonized" as timestamps...

FYI: As workaround until this is fixed:

Just add this class or just the bean definitions to your project, then the default spring boot registration for the object mapper is used again. Even with HAL

 @Configuration
    static class Cfg {
/* Use this if you want to have a not-hal enabled Objectmapper as default
        @Bean
        @Primary
        public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
            return builder.createXmlMapper(false).build();
        }
*/
// use this if you want to have a hal enabled objectmapper everywhere
        @Bean @Primary
        public ObjectMapper objectMapper(@Qualifier("_halObjectMapper") ObjectMapper objectMapper) {
            return objectMapper;
        }

        @Bean
        public HalObjectMapperConfigurer halObjectMapperConfigurer() {
            return new HalObjectMapperConfigurer();
        }

        /**
         * {@link BeanPostProcessor} to apply any {@link Jackson2ObjectMapperBuilder}
         * configuration to the HAL {@link ObjectMapper}.
         */
         class HalObjectMapperConfigurer
                implements BeanPostProcessor, BeanFactoryAware {

            private BeanFactory beanFactory;

            @Override
            public Object postProcessBeforeInitialization(Object bean, String beanName)
                    throws BeansException {
                if (bean instanceof ObjectMapper && "_halObjectMapper".equals(beanName)) {
                    postProcessHalObjectMapper((ObjectMapper) bean);
                }
                return bean;
            }

            private void postProcessHalObjectMapper(ObjectMapper objectMapper) {
                try {
                    Jackson2ObjectMapperBuilder builder = this.beanFactory
                            .getBean(Jackson2ObjectMapperBuilder.class);
                    builder.configure(objectMapper);
                } catch (NoSuchBeanDefinitionException ex) {
                    // No Jackson configuration required
                }
            }

            @Override
            public Object postProcessAfterInitialization(Object bean, String beanName)
                    throws BeansException {
                return bean;
            }

            @Override
            public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
                this.beanFactory = beanFactory;
            }

        }

    }

Is there a reason why there is a second objectmapper?

@SympathyForTheDev
Copy link

SympathyForTheDev commented Jun 8, 2017

Hello, I'm using SpringBoot 1.5.3.RELEASE and spring-hateoas 0.23

I have the same issue and the work around from @pcornelissen
doesn't work for me

here my config class :

@Configuration
public class JacksonConfig
{

    @Bean
    @Primary
    public Jackson2ObjectMapperBuilder jacksonBuilder()
    {
        return new Jackson2ObjectMapperBuilder()
                .indentOutput(false)
                .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS);
    }

    @Bean
    public Module javaTimeModule()
    {
        return new JavaTimeModule();
    }

    // use this if you want to have a hal enabled objectmapper everywhere
    @Bean
    @Primary
    public ObjectMapper objectMapper(@Qualifier("_halObjectMapper") final ObjectMapper objectMapper)
    {
        return objectMapper;
    }

    @Bean
    public HalObjectMapperConfigurer halObjectMapperConfigurer()
    {
        return new HalObjectMapperConfigurer();
    }

    /**
     * {@link BeanPostProcessor} to apply any {@link Jackson2ObjectMapperBuilder}
     * configuration to the HAL {@link ObjectMapper}.
     */
    class HalObjectMapperConfigurer
            implements BeanPostProcessor, BeanFactoryAware
    {

        private BeanFactory beanFactory;

        @Override
        public Object postProcessBeforeInitialization(final Object bean, final String beanName)
                throws BeansException
        {
            if (bean instanceof ObjectMapper && "_halObjectMapper".equals(beanName)) {
                postProcessHalObjectMapper((ObjectMapper) bean);
            }
            return bean;
        }

        private void postProcessHalObjectMapper(final ObjectMapper objectMapper)
        {
            try {
                final Jackson2ObjectMapperBuilder builder = this.beanFactory
                        .getBean(Jackson2ObjectMapperBuilder.class);
                builder.configure(objectMapper);
            }
            catch (final NoSuchBeanDefinitionException ex) {
                // No Jackson configuration required
            }
        }

        @Override
        public Object postProcessAfterInitialization(final Object bean, final String beanName)
                throws BeansException
        {
            return bean;
        }

        @Override
        public void setBeanFactory(final BeanFactory beanFactory) throws BeansException
        {
            this.beanFactory = beanFactory;
        }

    }
}

any ideas why ?

@gregturn
Copy link
Contributor

gregturn commented Jun 8, 2017

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.

@pfridberg
Copy link

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.

@maxtuzz
Copy link

maxtuzz commented Oct 13, 2017

This is definitely a problem as of Spring Boot 2 M4 as well. My API requires null values to not be serialized but I also needed to use Spring Boot 2 to support Spring Data Elasticsearch with ES 5.X.

After isolating the issue to being a Hateoas related, the following configuration class was able to sort out my problems:

@Configuration
public class JacksonConfiguration {
    private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper";

    private final BeanFactory beanFactory;

    @Autowired
    public JacksonConfiguration(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = (ObjectMapper) beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

        return mapper;
    }
}

@ashaffer1903
Copy link

ashaffer1903 commented Jan 5, 2018

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;
	}
}

@danielgblanco
Copy link

danielgblanco commented Mar 6, 2018

I just ran into this problem, I followed a similar solution to @ashaffer1903 but with an autowired Jackson2ObjectMapperBuilder to configure the HAL ObjectMapper:

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

    private final Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder;

    @Autowired
    public ObjectMapperBeanPostProcessor(Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder) {
        this.jackson2ObjectMapperBuilder = jackson2ObjectMapperBuilder;
    }


    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Nullable
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (HAL_OBJECT_MAPPER_BEAN.equals(beanName)) {
            jackson2ObjectMapperBuilder.configure((ObjectMapper) bean);
        }
        return bean;
    }
}

@kschulst
Copy link

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

@odrotbohm
Copy link
Member

We've just pushed some significant configuration changes to Spring HATEOAS (both on master as well on the bugfix branch for the upcoming 0.25.0) in the context of #719 #723. I've adapted Spring Data REST to work with these changes (coming in Lovelace) and verified our Spring Data examples work with these changes on Boot 2.0.3. Find the details of what has changed in the tickets I linked above.

I'd like to encourage everyone who brought up issues in this ticket to try to upgrade to Spring HATEOAS 0.25.0.BUILD-SNAPSHOT (it doesn't contain any major changes but the ones just described), give it a spin and report problems you (still see) in #719.

@sbley
Copy link

sbley commented Sep 7, 2018

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?

@odrotbohm
Copy link
Member

odrotbohm commented Sep 7, 2018

As described in the commit that introduces the change, Spring HATEOAS will now reuse an ObjectMapper bean found in the ApplicationContext to create copies from it to augment the setup to include serializers for the media type specific customizations.

@gregturn
Copy link
Contributor

Resolved via #719 and #723.

@ghost
Copy link

ghost commented Jan 16, 2019

Hello,

Is there any clear instruction on how to add a custom media type (*+hal+json) to HEAEOAS 0.25 as per the issue above that are now marked as resolved? I can see changes have been made but cannot see instruction on how to configure them correctly?

@gregturn
Copy link
Contributor

This issue was about customizing a Jackson ObjectMapper (presumably to alter indentation etc) not registering another media type.

We haven’t hammered out the APIs to simplify the process of creating your own media type

But you’re free to look up how Spring HATEOAS registers a custom message converter for each custom Jackson module and attempt the same.

PS PRs welcome!

@ghost
Copy link

ghost commented Jan 16, 2019

@gregturn I successfully did this with previous versions of Spring HATEOS by getting the bean named _halObjectMapper. When changing this to jacksonObjectMapper I have no success

@odrotbohm
Copy link
Member

Spring HATEOAS picks up the primary ObjectMapper instance available in the ApplicationContext. I.e. if you're using Boot, the means to configure it should follow the Boot documentation.

@ghost
Copy link

ghost commented Jan 16, 2019

Here is my configuration

@Configuration
public class HalConfiguration extends WebMvcConfigurationSupport {

    private static final Logger log = LoggerFactory.getLogger(HalConfiguration.class);

    @Autowired
    private ObjectMapper objectMapper;

    private HttpMessageConverter customConverters() {

        final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();

        converter.setSupportedMediaTypes(Collections.singletonList(
                new MediaType("application", "*+hal+json")
        ));

        ObjectMapper mapper = objectMapper.copy();
        mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        mapper.registerModule(new Jackson2HalModule());
        converter.setObjectMapper(mapper);

        return converter;
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(customConverters());
        super.addDefaultHttpMessageConverters(converters);
    }
}

Putting a break point after the super.addDefaultHttp... shows me 9 converters and the one I created above is present

and I have @RequestMapping(produces = [MediaType.APPLICATION_JSON_VALUE,"application/vnd.csc.myapp.v2+hal+json"]) on my controller

and still I am getting this response

    "links": [
        {
            "rel": "self",
            "href": "/assets?id=xxxyyy",
            "hreflang": null,
            "media": null,
            "title": null,
            "type": null,
            "deprecation": null
        }
    ]

Any help much appreciated

@gregturn
Copy link
Contributor

gregturn commented Feb 7, 2019

After #728 is reviewed and merged to master, I have something in progress that will make it much simpler to register your own media types.

@gregturn
Copy link
Contributor

gregturn commented Mar 5, 2019

See #833 for custom media types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests