Using Jackson in Jersey with multiple configured ObjectMappers - jersey

Is it possible to setup Jersey using Jackson for serialization/deserialization using multiple configured ObjectMappers?
What I would like to be able to do is register a "default" Jackson ObjectMapper and then have the ability to register another feature which provides an ObjectMapper with some specialized configuration which under certain circumstance will "override" the "default" ObjectMapper.
For example, this ContextResolver would be for the "default" mapper:
#Provider
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
public class JacksonMapperProvider implements ContextResolver<ObjectMapper> {
private final ObjectMapper mObjectMapper;
public JacksonMapperProvider() {
mObjectMapper = createMapper();
}
protected abstract ObjectMapper createMapper() {
ObjectMapper mapper = createMapper();
return mapper
.setSerializationInclusion(Include.ALWAYS)
.configure(JsonParser.Feature.ALLOW_COMMENTS, true)
.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true)
.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true)
.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
}
#Override
public ObjectMapper getContext(Class<?> type) {
return mObjectMapper;
}
}
And this ContextResolver would be to override the "default" mapper:
#Provider
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
public class SpecializedMapperProvider implements ContextResolver<ObjectMapper> {
private final ObjectMapper mObjectMapper;
public SpecializedMapperProvider() {
mObjectMapper = createMapper();
}
protected abstract ObjectMapper createMapper() {
ObjectMapper mapper = createMapper();
return mapper
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"))
.registerModule(new SpecializedModule1())
.registerModule(new SpecializedModule2());
}
#Override
public ObjectMapper getContext(Class<?> type) {
if(SomeType.isAssignableFrom(type)) {
return mObjectMapper;
}
return null;
}
}
I see in the JacksonJsonProvider code that Jackson supports ObjectMapper provider injection/resolution. However, in practice, what I am seeing is that the "order" of the providers seems random (I'm guessing it's not, but I can't sort out how to control the order). Sometimes the "override" comes before the "default" and everything works, but on the next server startup the order changes.
I have attempted to get this to work in a number of ways including:
Registering the ContextResolver<ObjectMapper> implementations manually (in differing orders)
Registering the ContextResolver<ObjectMapper> implementations via #Provider annotations
Specifying a priority when registering
I am using the following:
Jersey 2.8
Jackson 2.3.3
Perhaps I am taking a completely incorrect approach?
Is there a better way to achieve what I am trying to do?
Maybe I should just define two separate JAX-RS applications and have a single ObjectMapper configuration for each?

You can configure the order of providers, but it would actually be best to use one provider in this situation:
#Provider
public class JacksonMapperProvider implements ContextResolver<ObjectMapper> {
private final ObjectMapper defaultMapper;
private final ObjectMapper specializedMapper;
public JacksonMapperProvider() {
defaultMapper = createDefaultMapper();
specializedMapper = createSpecializedMapper();
}
private static ObjectMapper createDefaultMapper() {
return new ObjectMapper()
.setSerializationInclusion(Include.ALWAYS)
.configure(JsonParser.Feature.ALLOW_COMMENTS, true)
.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true)
.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true)
.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
}
private static ObjectMapper createSpecializedMapper() {
return new ObjectMapper()
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"))
.registerModule(new SpecializedModule1())
.registerModule(new SpecializedModule2());
}
#Override
public ObjectMapper getContext(Class<?> type) {
if (SomeType.isAssignableFrom(type)) {
return specializedMapper;
}
else {
return defaultMapper;
}
}
}

The newest way is
new ObjectMapper().configure(com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
to get ALLOW_UNQUOTED_FIELD_NAMES in recent versions of jackson.

Related

LocalDateTime not serializing based on given serializer registered with JavaTimeModule

I'm facing an issue where Spring boot (v2.6.13) is not parsing LocalDateTime based on a registered serializer, the response of LocalDateTime of RestController is always an array of integers.
#Bean
public Module javaTimeModule() {
JavaTimeModule module = new JavaTimeModule();
module.addSerializer(new CustomLocalDateTimeSerializer());
return module;
}
class CustomLocalDateTimeSerializer extends StdSerializer<LocalDateTime> {
private static DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd");
protected CustomLocalDateTimeSerializer() {
super(LocalDateTime.class);
}
#Override
public void serialize(
LocalDateTime localDateTime,
JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(localDateTime.format(formatter));
}
}
Notes:
Injecting object mapper and serialize the object returns the correct format.
I've defined an object mapper annotated with #Primary, but still facing the same issue.
I want to configure everything globally - don't wanna use #JsonSerialize on each attribute-
it seems like Spring is using a different object mapper for serializing a method returned object.
I've found the issue and resolved it.
I have a WebMvcConfigurationSupport configuration which was overriding the configured object mapper.
I've solved it using following code
private final ObjectMapper objectMapper;
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters)
{
var converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(objectMapper);
converters.add(converter);
super.configureMessageConverters(converters);
}

Using ContextResolver to register ObjectMapper in runtime

We're using JAX-RS (Jersey implementation) to call to external systems.
On JAX-RS Client's creation I'm registering the below context resolver to use custom ObjectMapper:
public class JacksonObjectMapperProvider implements ContextResolver<ObjectMapper>
{
#Override
public ObjectMapper getContext(Class<?> type)
{
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
mapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
mapper.configure(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS, true);
mapper.setSerializationInclusion(Include.NON_NULL);
return mapper;
}
}
But I don't want to have the ObjectMapper defined in JacksonObjectMapperProvider. I want JacksonObjectMapperProvider to be able to retrieve it in runtime from somewhere, or have someone set the ObjectMapper on JacksonObjectMapperProvider.
I cannot do something like bellow, because the ObjectMapper is defined on some instance that creating the jax-rs Client. And here I don't have a reference to that instance:
public class JacksonObjectMapperProvider implements ContextResolver<ObjectMapper>
{
#Override
public ObjectMapper getContext(Class<?> type)
{
return someService.getObjectMapper();
}
}
Is there another way to do it?
Is there a way to pass data to JacksonObjectMapperProvider when registering on Client?
The solution is easier then I thought, instead of registering the class:
ClientConfig clConfig = new ClientConfig();
client.register(JacksonObjectMapperProvider.class);
as I did, you can register an instance of the class, and on instance creation pass whatever you want:
ClientConfig clConfig = new ClientConfig();
client.register(new JacksonObjectMapperProvider(objectMapper));
The updated provider:
public class JacksonObjectMapperProvider implements ContextResolver<ObjectMapper>
{
private ObjectMapper mapper;
public JacksonObjectMapperProvider(ObjectMapper mapper)
{
this.mapper = mapper;
}
#Override
public ObjectMapper getContext(Class<?> type)
{
return mapper;
}
}

#XmlElement(name = "", required = true) not working with springboot validator

Initially I had an old xsd and from that I generated classes using xjc. I am trying to use JaxB annotation based validation but it seems Validation is ignoring required. Some of the xml nodes are mandatory but as validator is not using required marked in #XMLElement so object is going to backend system and then it fails.
#XmlElement(name = "", required = true)
#Configuration
public class AppConfig {
#Bean
public JaxbAnnotationModule jaxbAnnotationModule() {
return new JaxbAnnotationModule();
}
#Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JaxbAnnotationModule());
mapper.setAnnotationIntrospector(new JaxbAnnotationIntrospector(mapper.getTypeFactory()));
mapper.setSerializationInclusion(Include.NON_NULL);
return mapper;
}
}
Validation done using
#Autowired
private Validator validator;
...Some code...
Set<ConstraintViolation<Request>> violations = validator.validate(request);

What is the suggested way to modify / override / extend built-in Jackson 2 ObjectMapper in Spring Webflux?

Currently I have a minimalistic Spring / Netty, Reactor / Web Flux project with Jackson libraries
#Configuration
public class EmbeddedSpringServer extends DelegatingWebFluxConfiguration {
#Bean
MyController controller() {
return new AdminController();
}
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(EmbeddedSpringServer.class);
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(applicationContext).build();
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create("0.0.0.0", 8082).newHandler(adapter).subscribe();
applicationContext.registerShutdownHook();
}
}
build.gradle:
compile 'org.springframework:spring-context:5.0.2.RELEASE'
compile 'org.springframework:spring-web:5.0.2.RELEASE'
compile 'org.springframework:spring-webflux:5.0.2.RELEASE'
compile 'io.projectreactor.ipc:reactor-netty:0.7.2.RELEASE'
compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.3'
Controller class works fine (it returns a Mono<> with a DTO type).
Because Jackson is present in classpath Web Flux automatically creates an Object Mapper instance via DefaultServerCodecConfigurer however it's not clear how to override object mapper instance, because most Web Flux configuration classes are package private.
What I'd like to achieve is to create my own object mapper to add custom LocalDateTime serialization implemented in jackson-modules-java8
ObjectMapper mapper = new ObjectMapper()
.registerModule(new ParameterNamesModule())
.registerModule(new Jdk8Module())
.registerModule(new JavaTimeModule())
;
The problem is that's it's not clear how to modify Jackson2JsonEncoder created in package private org.springframework.http.codec.support.AbstractCodecConfigurer.AbstractDefaultCodecs.
You can disable that Jackson feature right from your application.properties file with:
spring.jackson.serialization.write-dates-as-timestamps=false
It turned out simpler than I thought initially as DelegatingWebFluxConfiguration already has a configureHttpMessageCodecs method overriding which is enough
#Configuration
public class EmbeddedSpringServer extends DelegatingWebFluxConfiguration {
#Bean
MyController controller() {
return new MyController();
}
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(EmbeddedSpringServer.class);
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(applicationContext).build();
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create("0.0.0.0", 8082).newHandler(adapter).subscribe();
applicationContext.registerShutdownHook();
}
#Bean
ObjectMapper objectMapper(){
return new ObjectMapper()
.registerModule(new ParameterNamesModule())
.registerModule(new Jdk8Module())
.registerModule(new JavaTimeModule());
}
#Override
protected void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper()));
configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper()));
}
}
as for the Spring Boot I think it can be also achieved by returning a webFluxConfigurer bean
#Bean
WebFluxConfigurer webFluxConfigurer(ObjectMapper objectMapper) {
return new WebFluxConfigurer() {
#Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper());
configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper());
}
};
}
as they are picked by DelegatingWebFluxConfiguration created by #EnableWebFlux automatically.
N.B. default implementation of Jackson2ObjectMapperBuilder already registers these date modules automatically the problem with dates was not related, I ended with
#Override
protected void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(Jackson2ObjectMapperBuilder
.json()
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.build()));
}
to achieve simple date serialization.

Spring Hibernate Jackson Hibernate5Module

I have set up spring 4.3.1 with Hibernate 5.1.0 and Jackson 2.7.5
I had some lazy init Exceptions because the Jackson ObjectMapper tries to convert my Objects to late when I am out of the Transactional Service.
Therefore I have read the Hibernate5Module for Jackson.
After adding the Module I do not get lazy Exceptions BUT all #JsonView Annotations are ignored and my lazy collections are 'null'
public class SpringWebConfig extends WebMvcConfigurerAdapter {
...
#Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
for (HttpMessageConverter<?> converter : converters) {
if (converter instanceof org.springframework.http.converter.json.MappingJackson2HttpMessageConverter) {
ObjectMapper mapper = ((MappingJackson2HttpMessageConverter) converter).getObjectMapper();
mapper.registerModule(new Hibernate5Module());
}
}
}
}
Is there anything I am doing wrong?
The Hibernate5Module should initialize the lazy collections ...
By creating your own ObjectMapper, you're overriding the one Spring Boot would set up, which would include a bunch of useful modules, such as Jdk8 module.
What you should do instead, is just add the Hibernate5() module to the Application Context and Spring Boot will automatically add it to the ObjectMapper that it sets up. Like this in any #Configuration class:
#Bean
public Hibernate5Module hibernate5Module() {
return new Hibernate5Module();
}
Got it to work with the following
#EnableWebMvc
#Configuration
#ComponentScan({ "..." })
public class SpringWebConfig extends WebMvcConfigurerAdapter {
#Autowired
SessionFactory sf;
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Hibernate5Module module = new Hibernate5Module(sf);
module.disable(Feature.USE_TRANSIENT_ANNOTATION);
module.enable(Feature.FORCE_LAZY_LOADING);
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.modulesToInstall(module);
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
}
}
I manage to make it work with the below implementation
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Hibernate5Module module = new Hibernate5Module(); // or Hibernate4Module ... depends on hibernate version you are using
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
converters.add(new MappingJackson2HttpMessageConverter(mapper));
}
jackson-datatype-hibernate5 bring many solutions but there are some default configurations as well.
Please have a look on
Below is the configuration I did as per my project requirements.
#Bean
public Hibernate5Module hibernateModule() {
Hibernate5Module module = new Hibernate5Module();
module.disable(Hibernate5Module.Feature.USE_TRANSIENT_ANNOTATION);
module.enable(Hibernate5Module.Feature.FORCE_LAZY_LOADING);
return module;
}

Resources