spring-cloud/spring-cloud-function

Custom ObjectMapper is not used

sonallux opened this issue ยท 9 comments

Describe the bug
With the latest spring-cloud-function 4.1.3 release a custom ObjectMapper bean is no longer respected when creating the FunctionCatalog in the ContextFunctionCatalogAutoConfiguration.java.

We need this behaviour, because must configure the ObjectMapper to use the snake-case naming strategy.

This regression is introduced by commit 8b66fd2 which fixed issue #1148.

Sample
If you really want an example I can provide some.

Possible Solution
I would like to propose the following solution to fix this issue. Adding a @ConditionalOnMissingBean on the bean definition of the JsonMapper here. With this change one can easily provide a custom JsonMapper Bean wrapping a custom ObjectMapper. This could also fix issue #1059.

We also stumbled over that issue. ๐Ÿ˜ž
You can mitigate it annotating a custom provided bean with @Primary.

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

If I understood the issue correctly, the reason for 8b66fd2 was that you need to configure some things on the ObjectMapper and don't want this to affect the original one from the context. Wouldn't it make sense then to just copy() the context ObjectMapper before modifying it? This way, you would get all the configuration from the context ObjectMapper like before, but would not change the original configuration.

I will submit a PR. This should restore the behavior from before the GH-1148 change without any additional configuration required. I still think #1160 is a valuable change as it would allow you to further configure the ObjectMapper to be used.

Same here.
We declare several Jackson Modules to add support for some classes serialization and deserialization.
Since upgrading to version 4.13 out tests fail.
I was able to trace the changes between 4.12 and 4.13 in ContextFunctionCatalogAutoConfiguration

4.12: our modules and jackson settings are used

private JsonMapper jackson(ApplicationContext context) {
	ObjectMapper mapper;
	try {
		mapper = context.getBean(ObjectMapper.class);
	}
	catch (Exception e) {
		mapper = new ObjectMapper();
	}
	mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
	mapper.configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, true);
	return new JacksonMapper(mapper);
}

4.13: our modules and jackson settings are no long used

private JsonMapper jackson(ApplicationContext context) {
	ObjectMapper mapper = new ObjectMapper();
	mapper.registerModule(new JavaTimeModule());
	mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
	mapper.configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, true);
	return new JacksonMapper(mapper);
}

We suffer from the same issue as well. As a workaround, we are overriding the behavior of the ObjectMapper used inside the JacksonMapper by calling the jacksonMapper.configureObjectMapper method. Hope this solution helps someone out there.

@Configuration
public class CustomJacksonMapperConfig {

    @Autowired
    public void customJacksonMapperConfig(JacksonMapper jacksonMapper) {
        jacksonMapper.configureObjectMapper(objectMapper -> {
            objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        });
    }
}

We also stumbled over that issue. ๐Ÿ˜ž You can mitigate it annotating a custom provided bean with @Primary.

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

it does not work, fails to create a bean.

The bean 'jsonMapper', defined in class path resource [org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration$JsonMapperConfiguration.class], could not be registered. A bean with that name has already been defined in com.foo.bar.config.ObjectMapperConfiguration

any updates on this issue?

I've been also hit by this issue today ๐Ÿ˜… . I think could be a nice fix the solution given by @sonallux

Thanks

We too are affected.
A workaround for the .. bean with that name has already been defined.. Exception was to create a custom AutoConfiguration that registeres a BeanPostProcessor overwriting the faulty Bean:

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.mycomp.framework.core.util.conditions.ConditionalOnDependencyVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration;
import org.springframework.cloud.function.json.JacksonMapper;
import org.springframework.context.annotation.Bean;

@AutoConfiguration
@AutoConfigureBefore({ContextFunctionCatalogAutoConfiguration.class})
public class WorkaroundForCloudFunctions {
    private static final Logger LOG = LoggerFactory.getLogger(WorkaroundForCloudFunctions.class);

    //TODO remove when https://github.com/spring-cloud/spring-cloud-function/pull/1162 is released and used.
    /**
     * Background: <a href="https://github.com/spring-cloud/spring-cloud-stream/issues/2977">Github Issue</a>
     */
    @ConditionalOnClass(JacksonMapper.class)
    @ConditionalOnDependencyVersion(groupId = "org.springframework.cloud",
                                    artifactId = "spring-cloud-function-context",
                                    versionRequirement = "4.1.3")
    @Bean
    public BeanPostProcessor jacksonMapperFix(ObjectMapper objectMapper) {
        return new BeanPostProcessor() {
            @Override
            public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
                if (bean instanceof JacksonMapper) {
                    LOG.warn("Injection custom JacksonMapper for spring-cloud-function-context.");
                    //replicate the modifications of ContextFunctionCatalogAutoConfiguration.JsonMapperConfiguration.jackson
                    var newOm = objectMapper.copy()
                                    .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
                                    .configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, true);
                    return new JacksonMapper(newOm);
                }
                return bean;
            }
        };
    }
}

Not the cleanest way to do it but effective.

This issue has been addressed
You can configure custom mapper as such

@Configuration
    @ConditionalOnProperty(value = "demo.jackson.mapper.enabled", havingValue = "true", matchIfMissing = true)
    public static class JacksonConfiguration {

        @Bean
        @Primary
        public JacksonMapper jacksonMapper(final ObjectMapper objectMapper) {
            objectMapper.registerModule(new JavaTimeModule());
            objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
            objectMapper.configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, true);
            return new JacksonMapper(objectMapper);
        }

    }