In my Spring Boot project I use a default Jackson ObjectMapper. I'd like to add new ObjectMapper to the Spring Context and start using it at new places, but also keep the default one. Adding new #Bean definition will override the default ObjectMapper. How can I add new ObjectMapper Bean without overriding the former one?
Yes, #ConditionalOnMissingBean is [hard-impossible] to hack. With a simple trick (asian philosophy), we can circumvent the problem/make it no problem at all:
Wrap your (1+, auto configured, #ConditionalOnMissing...) bean in something else/custom/a "wrapper". (at the costs of: referring to 1+/thinking about the difference/more complexity)
Mentioned MappingJackson2HttpMessageConverter (auto-config here) has this (built-in) capability (& purpose) to map to multiple object mappers in terms of "http conversion".
So with a (generic, e.g. java.util.Map based) thing like:
class MyWrapper<K, V> {
final Map<K, V> map;
public MyWrapper(Map<K, V> map) {
this.map = map;
}
public Map<K, V> getMap() {
return map;
}
}
We can go wire it:
#Bean
MyWrapper<String, ObjectMapper> myStr2OMWrapper(/*ObjectMapper jacksonOm*/) {
return new MyWrapper(Map.of(
// DEFAULT, jacksonOm,
"foo", fooMapper(),
"bar", barMapper()
));
}
..where fooMapper() and barMapper() can refer to (static/instance) no-bean methods:
private static ObjectMapper fooMapper() {
return new ObjectMapper()
.configure(SerializationFeature.INDENT_OUTPUT, true) // just a demo...
.configure(SerializationFeature.WRAP_ROOT_VALUE, true); // configure/set as see fit...
}
private static ObjectMapper barMapper() {
return new ObjectMapper()
.configure(SerializationFeature.INDENT_OUTPUT, false) // just a demo...
.configure(SerializationFeature.WRAP_ROOT_VALUE, false); // configure/set more...
}
(Already) testing/using time:
package com.example.demo;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
#SpringBootTest
class DemoAppTests {
#Autowired
MyWrapper<String, ObjectMapper> my;
#Autowired
ObjectMapper jacksonOM;
#Test
void contextLoads() {
System.err.println(jacksonOM);
Assertions.assertNotNull(jacksonOM);
my.getMap().entrySet().forEach(e -> {
System.err.println(e);
Assertions.assertNotNull(e.getValue());
});
}
}
Prints (e.g.)
...
com.fasterxml.jackson.databind.ObjectMapper#481b2f10
bar=com.fasterxml.jackson.databind.ObjectMapper#577bf0aa
foo=com.fasterxml.jackson.databind.ObjectMapper#7455dacb
...
Results:
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
...
Sorry this test dosn't verify (individual) configuration, but only: (visually different) not null object mappers.
How to enable (multiple!) my.custom.jackson.* auto configuration, is a more complex question... (it is not as easy as e.g. my.custom.datasource.* config ;(
With:
#Bean
#Primary // ! for auto config, we need one primary (whether it is "spring.jackson" ... adjust;)
#ConfigurationProperties("spring.jackson")
JacksonProperties springJacksonProps() {
return new JacksonProperties();
}
#Bean
#ConfigurationProperties("foo.jackson")
JacksonProperties fooProps() {
return new JacksonProperties();
}
#Bean
#ConfigurationProperties("bar.jackson")
JacksonProperties barProps() {
return new JacksonProperties();
}
we can already load and differentiate (full blown) config like:
spring.jackson.locale=en_US
spring.jackson.time-zone=UTC
# ... all of spring.jackson #see org.springframework.boot.autoconfigure.jackson.JacksonProperties
foo.jackson.locale=en_US
foo.jackson.time-zone=PST
# ... just for demo purpose
bar.jackson.locale=de_DE
bar.jackson.time-zone=GMT+1
And also (no problem) pass them (props) to the according (static [foo|bar]Mapper) methods.... but then? (If you are good with it, you can stop reading here!:)
Unfortunately the according ("state of art") code (to wire JacksonProperties with "om builder") is not public (i.e. not extendable/pluggable).
Instead the auto configuration provides (if none defined/#ConditionalOnMissingBean):
a prototype Jackson2ObjectMapperBuilder bean, which (everytime when requested):
applies (i.e. receives) customization from all (known) Jackson2ObjectMapperBuilderCustomizer beans.
of which one (auto configured, order(0), package private) is the "standard" responsible for wiring (spring.jackson.* only) JacksonProperties to Jackson2ObjectMapperBuilder...
So the the simplest approach seems (up-to-date) to:
steel/adopt the code (not-/implementing Jackson2ObjectMapperBuilderCustomizer)
construct (from "stolen" + properties) according builders/mappers, as see fit.
e.g. (review+TEST before PROD!) non-interface, returns a Jackson2ObjectMapperBuilder, mimic the auto-configured, without applying (other) customizers/-ation:
// ...
import com.fasterxml.jackson.databind.Module; // !! not java.lang.Module ;)
// ...
private static class MyStolenCustomizer {
private final JacksonProperties jacksonProperties;
private final Collection<Module> modules;
// additionally need/want this:
private final ApplicationContext applicationContext;
// copy/adopt from spring-boot:
private static final Map<?, Boolean> FEATURE_DEFAULTS = Map.of(
SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false,
SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false
);
public MyStolenCustomizer(
ApplicationContext applicationContext,
JacksonProperties jacksonProperties,
Collection<Module> modules
) {
this.applicationContext = applicationContext;
this.jacksonProperties = jacksonProperties;
this.modules = modules;
}
// changed method signature!!
public Jackson2ObjectMapperBuilder buildCustom() {
// mimic original (spring-boot) bean:
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.applicationContext(applicationContext);
// without (additional!) customizers:
if (this.jacksonProperties.getDefaultPropertyInclusion() != null) {
builder.serializationInclusion(this.jacksonProperties.getDefaultPropertyInclusion());
}
if (this.jacksonProperties.getTimeZone() != null) {
builder.timeZone(this.jacksonProperties.getTimeZone());
}
configureFeatures(builder, FEATURE_DEFAULTS);
configureVisibility(builder, this.jacksonProperties.getVisibility());
configureFeatures(builder, this.jacksonProperties.getDeserialization());
configureFeatures(builder, this.jacksonProperties.getSerialization());
configureFeatures(builder, this.jacksonProperties.getMapper());
configureFeatures(builder, this.jacksonProperties.getParser());
configureFeatures(builder, this.jacksonProperties.getGenerator());
configureDateFormat(builder);
configurePropertyNamingStrategy(builder);
configureModules(builder);
configureLocale(builder);
configureDefaultLeniency(builder);
configureConstructorDetector(builder);
// custom api:
return builder; // ..alternatively: builder.build();
}
// ... rest as in https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java#L223-L341
To wire modules, we can (hopefully, as originally) rely on:
#Autowired
ObjectProvider<com.fasterxml.jackson.databind.Module> modules
To initialize them like:
#Bean
MyStolenCustomizer fooCustomizer(ApplicationContext context, #Qualifier("fooProps") JacksonProperties fooProperties, ObjectProvider<Module> modules) {
return new MyStolenCustomizer(context, fooProperties, modules.stream().toList());
}
#Bean
MyStolenCustomizer barCustomizer(ApplicationContext context, #Qualifier("barProps") JacksonProperties barProperties, ObjectProvider<Module> modules) {
return new MyStolenCustomizer(context, barProperties, modules.stream().toList());
}
..and use them like:
#Bean
MyWrapper<String, Jackson2ObjectMapperBuilder> myStr2OMBuilderWrapper(
#Qualifier("fooCustomizer") MyStolenCustomizer fooCustomizer,
#Qualifier("barCustomizer") MyStolenCustomizer barCustomizer) {
return new MyWrapper(
Map.of(
"foo", fooCustomizer.buildCustom(),
"bar", barCustomizer.buildCustom()
)
);
}
...avoiding "double customization"/leaving JacksonAutoConfiguration enabled/intact/active.
Problem: time/updates(/external code)!
If you want just a default ObjectMapper to use, I wrote a small utility that has some static methods for serializing/deserializing JSON and it uses ObjectMapper inside. You don't have to inject any beans. just use the Util. Here is Javadoc for the JsonUtils class. It comes with the java Open Source MgntUtils library written and maintained by me. You can get it as Maven artifacts or in Github.
I, too, just faced a similar problem - I had already figured out how to make a new ObjectMapper bean, but I couldn't figure out, no matter what I did, how to keep that from Spring Boot's auto-configuration (so that it would continue to make the default one). In the end, I gave up and simply made the second bean (mimicking the default one), myself. I chose to name it, hopefully to avoid any collision, and to declare it #Primary, to be chosen as would have the default.
In either case, making an ObjectMapper is quite easy, as such:
#Bean("standardJsonObjectMapper") // named, though not necessary
#org.springframework.context.annotation.Primary // mimic default
public com.fasterxml.jackson.databind.ObjectMapper standardJsonObjectMapper() {
return
org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
.json()
.build();
}
That builder has MANY functions available for customization (like failOnUnknownProperties(boolean) and featuresToEnable(Object...)) - just choose the ones you want, and off you go!
The client can decide whether the PropertyNamingStrategy is SNAKE_CASE or CAMEL_CASE by adding it to the api parameter.
my idea is to do aop interceptor before entering the controller to customize ObjectMapper.
I have setPropertyNamingStrategy for ObjectMapper object but it only get first PropertyNamingStrategy set, values set after first time are not used.
#Aspect
#Component
#RequiredArgsConstructor
public class NamingJsonAspect {
private final ObjectMapper objectMapper;
#Pointcut("execution(public * com.nnv98..*Controller.*(..))")
private void namingJson() {}
#SneakyThrows
#Before("namingJson()")
public void doAround(JoinPoint proceedingJoinPoint) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert attributes != null;
HttpServletRequest request = attributes.getRequest();
String namingJson = request.getParameter("namingJson");
if(namingJson.equals("SNAKE_CASE")){
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
}else {
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.LOWER_CAMEL_CASE);
}
}
}
thank you
You should read the JavaDoc for ObjectMapper.
What you are seeing is expected behavior, as explained in the JavaDoc: configuration can only be done before first read/write usage. After first usage, changing the configuration may have no effect or may result in errors. The JavaDoc also explains how you can work around the limitation.
I want to upgrade my jersey version to 2.x from 1.x.
In my code I had:
#Provider
#Produces(MediaType.APPLICATION_JSON)
public class JAXBContextResolver implements ContextResolver<JAXBContext> {
private static final Class<?>[] classes = {
A.class, B.class, C.class, D.class, E.class,
F.class, G.class
};
private JAXBContext context;
public JAXBContextResolver() throws Exception {
context = new JSONJAXBContext(JSONConfiguration.natural()
.humanReadableFormatting(true).rootUnwrapping(true).build(),
classes);
}
public JAXBContext getContext(Class<?> objectType) {
return context;
}
}
But JSONJAXBContext and JSONConfiguration are not defined in jersey 2.x.
How can I make the change accordingly?
The question Where did JSONConfiguration go in Jersey 2.5.x? is not answering my question because it does not explain how do I add my class which I want to return as output
There is no need for this. You either are going to use MOXy or Jackson as your JSON provider in Jersey 2.x. For the latter, you configure with MoxyJsonConfig. For Jackson, you use ObjectMapper. Figure out which provider you are using, and configure the according object. Both can be configured in a ContextResolver like you're currently doing.
As far as your current configurations
You won't need to configure any classes with either of these.
Unwrapped objects are serialized by default.
And to pretty print you would do the following
Jackson
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
MOXy
MoxyJsonConfig config = new MoxyJsonConfig()
.setFormattedOutput(true);
Based on other issues that were actually resolved in Jersey 2.6, I suspect this might be a Jersey bug, but I wanted to vet it here first.
The following works as expected:
#Provider
public class ObjectMapperResolver implements ContextResolver<ObjectMapper> {
private ObjectMapper mapper;
#Value("${json.prettyPrint}")
private boolean prettyPrint = false;
public ObjectMapperResolver() {
mapper = new ObjectMapper();
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.configure(SerializationFeature.INDENT_OUTPUT, prettyPrint);
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
/**
* Push Joda de/serializers into the actual mapper
*/
#PostConstruct
private void configureJodaSerializers() {
mapper.registerModule(new JodaModule()
// Our deserializers are more forgiving
.addDeserializer(LocalDate.class, new CustomLocalDateDeserializer())
.addDeserializer(LocalTime.class, new CustomLocalTimeDeserializer())
// Custom serializer to avoid HH:mm:ss.SSS (we don't want millis)
.addSerializer(LocalTime.class, new LocalTimeSerializer()));
}
#Override
public ObjectMapper getContext(Class<?> type) {
return mapper;
}
}
In all my resources, my Joda date types are properly serialized. However, I wanted to grab the same ObjectMapper to use in a non-Jersey managed context (outside of my resources), so I added #Component to the above class with the intention of auto-wiring it elsewhere. After adding #Component (org.springframework.stereotype.Component), Jersey no longer picks up the ObjectMapper from the resolver and my date serialization goes back to the defaults.
Unless I completely misunderstand the annotations, I don't think giving Spring control of the life-cycle should impede Jersey's ability to pick up my resolver. Additionally worth noting is the fact that when we were on Jersey 1.9, we HAD to have #Component on there or else it would not get picked up. In order to get our upgrade from 1.9 to 2.6 working, I actually had initially removed it, but was hoping to put it back.
From my pom:
Java 1.7
Jackson 2.3.1
Jersey 2.6
Joda 2.1
Spring 4.0.1-RELEASE
I had a similar issue with a similar setup as your one.
While probably there's something wrong in Jersey 2.x Spring integration beahviour, i think you can do the follow:
Declare the object mapper as a Spring bean, so you can inject it via spring where you need it:
#Component
public class ObjectMapperBean extends ObjectMapper {
public ObjectMapperBean() {
super();
// Configuration here...
}
}
Then you write a Jersey context resolver for it:
#Provider
public class ObjectMapperContextResolver implements ContextResolver<ObjectMapper> {
#Autowired
private ObjectMapperBean objectMapper;
#Override
public ObjectMapper getContext(Class<?> type) {
return objectMapper;
}
}
Even if not declared as a component you will get the ObjectMapperBean injected in it.
Hope it helps!
I am using RESTEasy to implement a REST Service using JSON serialization. Currently, Dates are getting serialized to milliseconds since 1970. To improve compatibility, I would like to get my dates into one of two formats; milliseconds + timezone offset or ISO 8061.
It seems that RESTEasy used to use Jettison for JSON serialization, but from what I've been reading they've switch to Jackson ... all of this has made googling for help pretty hit or miss.
From what I can tell, I need to implement a ContextResolver along the lines of:
public class JacksonConfig impelments ContextResolver<ObjectMapper>
{
private final OBjectMapper objectMapper;
public JacksonConfig() throws Exception
{
objectMapper = new ObjectMapper.configure(
SerializationFeature.WRITE_DATE_AS_TIMESTAMPS, false);
}
#Override
public ObjectMapper getContext(Class<?> arg0)
{
return objectMapper;
}
}
The thing I haven't been able to find, is what do I do with this? Where do I put it?
So the larger questions are, am I heading in the right direction and are my assumptions correct?
You need to register your ContextResolver implementation with Resteasy. You can do this by annotating your class with the #Provider annotation and allowing Resteasy to automatically scan it during startup, registering it in web.xml, or registering it in a class that extends javax.ws.rs.core.Application (if that is how you are bootstrapping Resteasy).
Registering via Annotations
#Provider
public class JacksonConfig implements ContextResolver<ObjectMapper>
{
private final ObjectMapper objectMapper;
public JacksonConfig() throws Exception
{
objectMapper = new ObjectMapper.configure(
SerializationFeature.WRITE_DATE_AS_TIMESTAMPS, false);
}
#Override
public ObjectMapper getContext(Class<?> arg0)
{
return objectMapper;
}
}
Verify that classpath scanning is enabled in your web.xml file like so:
<context-param>
<param-name>resteasy.scan</param-name>
<param-value>true</param-value>
</context-param>
NOTE: If you are deploying this in JBoss 7 do not set the resteasy.scan context parameter as it is enabled by default.
Registering via web.xml
Add the following context parameter to your web.xml file. The value of the parameter should be the fully qualified class name of your ContextResolver.
<context-param>
<param-name>resteasy.providers</param-name>
<param-value>foo.contextresolver.JacksonConfig</paramvalue>
</context-param>
Registering via Application
If you are using an Application class to configure Resteasy you can add your provider to the set of services and providers to register with Resteasy like so:
public class MyApp extends Application
{
#Override
public Set<Class<?>> getClasses()
{
HashSet<Class<?>> set = new HashSet<Class<?>>(2);
set.add(JacksonConfig.class);
set.add(MyService.class);
return set;
}
}
More on standalone configuration HERE
Using with the JSR310 (new api date) - LocalDate, LocalDateTime, LocalTime
Add dependence:
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.4.0</version>
</dependency>
And create a provider to register the module:
#Provider
public class JacksonConfig implements ContextResolver<ObjectMapper> {
private final ObjectMapper objectMapper;
public JacksonConfig() throws Exception {
objectMapper = new ObjectMapper()
.disable( SerializationFeature.WRITE_DATES_AS_TIMESTAMPS )
.disable( SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS )
.setSerializationInclusion( JsonInclude.Include.NON_NULL )
.registerModule( new JSR310Module() );
}
#Override
public ObjectMapper getContext( Class<?> arg0 ) {
return objectMapper;
} }
Just annotate your fields with (note the string literal could be externalized/referred from a constant):
#javax.json.bind.annotation.JsonbDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
Date myDate;