Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I define a custom ObjectMapper bean without overriding the one used by Spring Boot

I have a Spring Boot web app with several @RestController classes. I like the default json format returned by my REST controllers.

For use in my DAO beans (which do json serialization and deserialization ), I have created a custom ObjectMapper:

@Configuration
public class Config{

  @Bean
  public ObjectMapper getCustomObjectMapper() {
    final ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    objectMapper.setPropertyNamingStrategy(new PropertyNamingStrategy.SnakeCaseStrategy());
    return objectMapper;
  }
}

And in each of my DAO classes I autowire my custom ObjectMapper:

@Repository
@Transactional
public class MyDaoImpl implements MyDao {

@Autowired
ObjectMapper objectMapper

//Dao implementation...

}

This all works fine. The problem is that my custom ObjectMapper gets automatically picked up by Spring and is used for serializing REST responses.
This is undesirable. For REST controllers I want to keep the ObjectMapper that Spring creates by default.

How can I tell Spring Boot to not detect and not use my custom ObjectMapper bean for its own internal workings?

like image 628
Doron Gold Avatar asked Mar 06 '18 19:03

Doron Gold


4 Answers

The Simone Pontiggia answer is in the correct direction. You should create one @Primary bean, which Spring will use in its internals, and then to create your own ObjectMapper beans and autowired them using @Qualifier.

The problem here is that, creating default bean like:

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

Won't actually work as expected, because the Spring default ObjectMapper has additional configurations. The correct way to create default ObjectMapper that will be used by spring, is:

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

You can find more information about the Spring default ObjectMapper here: https://docs.spring.io/spring-boot/docs/current/reference/html/howto-spring-mvc.html under 79.3 Customize the Jackson ObjectMapper

like image 95
martin.kosturkov Avatar answered Oct 18 '22 20:10

martin.kosturkov


You can provide a standard ObjectMapper and your customized object mapper, and set the standard as @Primary.

Then gives your custom ObjectMapper a name and use it with @Qualifier annotation.

@Configuration
public class Config{

  //This bean will be selected for rest
  @Bean
  @Primary
  public ObjectMapper stdMapper(){
     return new ObjectMapper();
  }

  //You can explicitly refer this bean later
  @Bean("customObjectMapper")
  public ObjectMapper getCustomObjectMapper() {
    final ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    objectMapper.setPropertyNamingStrategy(new PropertyNamingStrategy.SnakeCaseStrategy());
    return objectMapper;
  }
}

Now you can reference your custom mapper

@Repository
@Transactional
public class MyDaoImpl implements MyDao {

@Autowired
@Qualifier("customObjectMapper")
ObjectMapper objectMapper

//Dao implementation...

}

@Resource("custonmObjectMapper") will do the same of @Autowired and @Qualifier together

like image 22
Simone Pontiggia Avatar answered Oct 18 '22 20:10

Simone Pontiggia


Since I didn't want to touch Spring's default ObjectMapper, creating a @Primary ObjectMapper to shadow Spring's default ObjectMapper was out of the question.

Instead, what I ended up doing is creating a BeanFactoryPostProcessor which registers in Spring's context a custom, non primary ObjectMapper:

@Component
public class ObjectMapperPostProcessor implements BeanFactoryPostProcessor {

public static final String OBJECT_MAPPER_BEAN_NAME = "persistenceObjectMapper";

@Override
public void postProcessBeanFactory(final ConfigurableListableBeanFactory beanFactory) {
    final AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder
        .genericBeanDefinition(ObjectMapper.class, this::getCustomObjectMapper)
        .getBeanDefinition();
    // Leave Spring's default ObjectMapper (configured by JacksonAutoConfiguration)
    // as primary
    beanDefinition.setPrimary(false);
    final AutowireCandidateQualifier mapperQualifier = new AutowireCandidateQualifier(PersistenceObjectMapper.class);
    beanDefinition.addQualifier(mapperQualifier);
    ((DefaultListableBeanFactory) beanFactory).registerBeanDefinition(OBJECT_MAPPER_BEAN_NAME, beanDefinition);
}

private ObjectMapper getCustomObjectMapper() {
    final ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    objectMapper.setPropertyNamingStrategy(new PropertyNamingStrategy.SnakeCaseStrategy());
    return objectMapper;
}
}

As can be seen in the code above, I also assigned a qualifier to my custom ObjectMapper bean.
My qualifier is an annotation which is annotated with @Qualifier:

@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface PersistenceObjectMapper {
}

I can then autowire my custom ObjectMapper using my custom annotation, like this:

@Repository
public class MyDao {
@Autowired
public MyDao(DataSource dataSource, @PersistenceObjectMapper ObjectMapper objectMapper) {
// constructor code
}
like image 39
Doron Gold Avatar answered Oct 18 '22 20:10

Doron Gold


You can create:

public class MapperUtils {

    private static final ObjectMapper mapper = new ObjectMapper();

    public static <T> T parseResponse(byte[] byteArrray, Class<T> parseType) throws JsonParseException, JsonMappingException, IOException {
        return mapper.readValue(byteArrray, parseType);
    }
}

ObjectMapper is thread-safe. However, some people discourage having single instance because of performance issues (Should I declare Jackson's ObjectMapper as a static field? ).

like image 34
Justinas Jakavonis Avatar answered Oct 18 '22 20:10

Justinas Jakavonis