How to serialize nested ObjectId to String with Jackson? - java

There are many questions concerning conversion from ObjectId to String with jackson. All answers suggest either creating own JsonSerializer<ObjectId> or annotating the ObjectId field with #JsonSerialize(using = ToStringSerializer.class).
However, I have a map that sometimes contains ObjectIds, i.e.:
class Whatever {
private Map<String, Object> parameters = new HashMap<>();
Whatever() {
parameters.put("tom", "Cat");
parameters.put("jerry", new ObjectId());
}
}
I want jackson to convert it to:
{
"parameters": {
"tom": "cat",
"jerry": "57076a6ed1c5d61930a238c5"
}
}
But I get:
{
"parameters": {
"tom": "cat",
"jerry": {
"date": 1460103790000,
"machineIdentifier": 13747670,
"processIdentifier": 6448,
"counter": 10631365,
"time": 1460103790000,
"timestamp": 1460103790,
"timeSecond": 1460103790
}
}
}
I have registered the conversion (in Spring) with
public class WebappConfig extends WebMvcConfigurerAdapter {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder
.serializerByType(ObjectId.class, new ToStringSerializer());
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(builder.build());
converters.add(converter);
}
}
And the first-level ObjectIds are converted correctly. How to make jackson convert also the nested ones? Do I have to write custom converter for this map?
Keep in mind that this Map can be nested multiple times (i.e. contain another maps). I just want to convert ObjectId to String whenever jackson sees it.

I suppose that you are taking about org.bson.types.ObjectId from org.springframework.boot:spring-boot-starter-data-mongodb. Your code works perfectly fine for me. 1 thing i can see is that you don't show #Configuration annotation above WebappConfig.
Here is my demo project, can you test it on yours setup?
Application.java
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import org.bson.types.ObjectId;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Configuration
public static class WebappConfig extends WebMvcConfigurerAdapter {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder
.serializerByType(ObjectId.class, new ToStringSerializer());
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(builder.build());
converters.add(converter);
}
}
#RestController
public static class MyRestController {
#ResponseBody
#RequestMapping("/")
public Whatever method() {
return new Whatever();
}
}
public static class Whatever {
private Map<String, Object> parameters = new HashMap<>();
public Whatever() {
parameters.put("tom", "Cat");
parameters.put("jerry", new ObjectId());
}
public Map<String, Object> getParameters() {
return parameters;
}
public void setParameters(Map<String, Object> parameters) {
this.parameters = parameters;
}
}
}
Responce from 127.0.0.1:8080
{
"parameters": {
"tom": "Cat",
"jerry": "5709df1cf0d9550b4de619d2"
}
}
Gradle:
dependencies {
compile("org.springframework.boot:spring-boot-starter-data-mongodb")
compile("org.springframework.boot:spring-boot-starter-web")
}

thanks varren's answer, it works fine in springMvc's older version.
but since 5.0, WebMvcConfigurerAdapter was deprecated.
solution:
may not work solution: we can implements WebMvcConfigurer directly for mvc config. but some config may not work, because WebMvcConfigurationSupport's priority is higher.
suggest solution: we can extends WebMvcConfigurationSupport directly. imply configureMessageConverters method can add all kinds of custom HttpMessageConverters we need, and it can works fine before default converters.
spring framework is a amazing framework, I need to look it deeper after I got time.(●'◡'●)

Related

Dynamically update the #value annotated fields in spring

I am trying to dynamically update the #value annotated fields in my application.
First of all, this application has a custom property source, with source being a Map<Object, String>.
A timer is enabled to update the values after a minute interval.
package com.test.dynamic.config;
import java.util.Date;
import java.util.Map;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.util.StringUtils;
public class CustomPropertySorce extends EnumerablePropertySource<Map<String, Object>> {
public CustomPropertySorce(String name, Map<String, Object> source) {
super(name, source);
new java.util.Timer().schedule(new java.util.TimerTask() {
#Override
public void run() {
source.put("prop1", "yoyo-modified");
source.put("prop2", new Date().getTime());
System.out.println("Updated Source :" + source);
}
}, 60000);
}
#Override
public String[] getPropertyNames() {
// TODO Auto-generated method stub
return StringUtils.toStringArray(this.source.keySet());
}
#Override
public Object getProperty(String name) {
// TODO Auto-generated method stub
return this.source.get(name);
}
}
Initial values of source Map<String, Object> is supplied from the PropertySourceLocator. (This is not the real scenario, but I am trying to recreate the logic used here)
package com.test.dynamic.config;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
public class CustomPropertySourceLocator implements PropertySourceLocator {
#Override
public PropertySource<?> locate(Environment environment) {
Map<String, Object> source=new HashMap<String,Object>(){{put("prop1","yoyo");put("prop2",new Date().getTime());}};
return new CustomPropertySorce("custom_source",source);
}
}
RestController class where I inject these properties using #Value is given below.
environment.getProperty("prop1"); is supplying updated value, but not the #value annotated fields.
I also tried to inject a new property source updatedMap using the addFirst method of environment.propertySources() assuming that it will take precedence over the others. But that effort also went futile. any clue is much appreciated.
package com.test.dynamic.config.controller;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class DataController {
#Value("${prop1}")
private String propertyOne;
#Value("${prop2}")
private Long propertyTwo;
#Autowired
private ConfigurableEnvironment environment;
#GetMapping("/p1")
private String getProp1() {
System.out.println("~~~~>"+environment.getPropertySources());
environment.getPropertySources().forEach(ps -> {
if(ps.containsProperty("prop1") || ps.containsProperty("prop2")) {
System.out.println("*******************************************************");
System.out.println(ps.getName());
System.out.println(ps.getProperty("prop1"));
System.out.println(ps.getProperty("prop2"));
System.out.println("*******************************************************");
}
});
// env.get
return propertyOne;
// return environment.getProperty("prop1");
}
#GetMapping("/p2")
private Long getProp2() {
System.out.println("~~~~>"+environment.getPropertySources());
// env.get
return propertyTwo;
// return environment.getProperty("prop1");
}
#GetMapping("/update")
public String updateProperty() {
Map<String, Object> updatedProperties = new HashMap<>();
updatedProperties.put("prop1", "Property one modified");
MapPropertySource mapPropSource = new MapPropertySource("updatedMap", updatedProperties);
environment.getPropertySources().addFirst(mapPropSource);
return environment.getPropertySources().toString();
}
}
If you think this is not the right way of injecting values to a RestController, please let me know. All possible alternate suggestions/best practices are accepted.
Thank you #flaxel. I used #RefreshScope to resolve this issue.
Posting the solution here if it helps someone with the same query.
In this particular case, I applied #RefreshScope on my Controller to refresh the bean with new values.
You can refer to this link before applying #RefreshScope to your bean.
It is the spring boot actuator that facilitates this refresh mechanism. So in order for this to work, you must have actuator in your classpath.
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-actuator', version: "${springboot_version}"
Then as discussed earlier, add RefreshScope to the bean that needs to be refreshed.
Finally, invoke the actuator/refresh endpoint to trigger the refresh.
If you want to programmatically do it, Autowire an instance of RefreshEndpoint class to your bean and invoke the refresh() method in it.
[Note: You don’t have to strictly follow this approach, but I am giving a clue that it can be Autowired]
#RefreshScope
#RestController
public class DataController {
#Value("${prop1}")
private String prop1;
#Autowired
private RefreshEndpoint refreshEndpoint;
#GetMapping("/p1")
public String getProp1(){
return prop1;
}
#getMappig("/refresh")
public void refresh(){
refreshEndpoint.refresh();
}
}
**************** MORE (if you are developing a library) ********************
What if you are developing a library and you have to get the RefreshEndpoint instance from the current ApplicationContext?
Simply Autowiring RefreshEndpoint may give you a null reference. Instead, you can get hold of the current ApplicationContext by the method given below. And use the ApplicationContext to get the RefreshEndpoint instance to invoke the refresh() method on it.
public class LocalApplicationContextFetcher implements
ApplicationContextInitializer<ConfigurableApplicationContext> {
private static ApplicationContext ctx;
#Override
public void initialize(ConfigurableApplicationContext applicationContext) {
ctx = applicationContext;
}
public static ApplicationContext getCtx() {
return ctx;
}
public static void refresh(){
ctx.getBean(RefreshEndpoint.class).refresh();
}
}
Finally, add this class to the spring.factories to get invoked by spring.
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.x.y.z.LocalApplicationContextFetcher

Generic methods in SpringBatch's Step chunk

I am actually using Spring Batch and I have been asking myself some questions about generic methods. Considering the following piece of code:
public <I, O> SimpleStepBuilder<I, O> chunk(int chunkSize) {
return new SimpleStepBuilder<I, O>(this).chunk(chunkSize);
}
If we check inside the source, a little bit further, we encounter:
public SimpleStepBuilder<I, O> reader(ItemReader<? extends I> reader) {
this.reader = reader;
return this;
}
I unterstand by chaining the calls like this:
#Bean
public Step step(final AccountReader accountReader, final AccountProcessor accountProcessor) {
return stepBuilderFactory.get("step")
.<COR_ACC_ACCOUNT, Account>chunk(10)
.reader(accountReader)
.processor(accountProcessor)
.writer(new JsonWriter<>())
.build();
}
That the reader() method will require an ItemProcessor<COR_ACC_ACCOUNT>
I found this kind of cool to preserve type safety.
Now my problem. Given the following code:
#Component
public class ItemDictionary {
private final Map<Class, ItemReader> itemReaders;
#Autowired
public ItemDictionary(final List<ItemReader> readers) {
itemReaders = readers.stream().collect(Collectors.toMap(
ItemReader::getClass,
Function.identity()
));
public <I> ItemReader<I> getReader(final Class clazz) {
return itemReaders.get(clazz);
}
}
I wanted to define the Step defined above like this:
#Bean
public Step step() {
return stepBuilderFactory.get("step")
.<COR_ACC_ACCOUNT, Account>chunk(10)
.reader(<COR_ACC_ACCOUNT>itemDictionary.getReader(AccountReader.class))
.processor(accountProcessor)
.writer(new JsonWriter<>())
.build();
}
itemDictionary is basically a Map containing all of the ItemReader implementations present in my Spring context.
This call <COR_ACC_ACCOUNT>itemDictionary.getReader(AccountReader.class) is rejected by the compilator as an illegal start of expression
Am I missing something ?
Is it still possible to preserve type safe check using ItemDictionnary ?
Yes, that syntax is actually wrong in java.
You can't preserve type safety here because you already loose it when you store your implementations to private final Map<Class, ItemReader> itemReaders
I would suggest that simplest solution will be doing type cast:
(ItemReader<COR_ACC_ACCOUNT>) itemDictionary.getReader(AccountReader.class)
Or we can thin about it further if you will explain what COR_ACC_ACCOUNT is. I have feeling that this is a generic type, right?
#Bean
public Step step() {
return stepBuilderFactory.get("step")
.<COR_ACC_ACCOUNT, Account>chunk(10)
.reader((ItemReader<COR_ACC_ACCOUNT>) itemDictionary.getReader(AccountReader.class))
.processor(accountProcessor)
.writer(new JsonWriter<>())
.build();
}
According to .<COR_ACC_ACCOUNT, Account>chunk(10), the type of items to read is COR_ACC_ACCOUNT, so AccountReader should return items of this type, in which case you don't need the cast here:
.reader(<COR_ACC_ACCOUNT>itemDictionary.getReader(AccountReader.class))
I tried your example and I don't have a compilation error:
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#Configuration
#EnableBatchProcessing
public class MyJob {
#Autowired
private JobBuilderFactory jobs;
#Autowired
private StepBuilderFactory steps;
#Bean
public ItemDictionary itemDictionary() {
return new ItemDictionary(Arrays.asList(new FooReader()));
}
#Bean
public ItemWriter<Bar> itemWriter() {
return null;
}
#Bean
public Step step() {
return steps.get("step")
.<Foo, Bar>chunk(5)
.reader(itemDictionary().getReader(Foo.class))
.writer(itemWriter())
.build();
}
#Bean
public Job job() {
return jobs.get("job")
.start(step())
.build();
}
public class ItemDictionary {
private Map<Class, ItemReader> itemReaders;
public ItemDictionary(final List<ItemReader> readers) {
itemReaders = readers.stream().collect(Collectors.toMap(
ItemReader::getClass,
Function.identity()
));
}
public <I> ItemReader<I> getReader(final Class clazz) {
return itemReaders.get(clazz);
}
}
class Foo { }
class Bar { }
class FooReader implements ItemReader<Foo> {
#Override
public Foo read() {
return null;
}
}
}
Hope this helps.

Can't import `configureRepositoryRestConfiguration` in Spring

I have extended my class from RepositoryRestMvcConfiguration according to documentation it has configureRepositoryRestConfiguration method which can be implemented but when I try to override this method I can't import it :|
Can anybody tell me Why this problem occurred?
EDIT : according to current version configureRepositoryRestConfiguration method is not avialble.. so what method should I used instead of this?
Here is my code
MSARepositoryRestMvcConfiguration.java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
import org.springframework.security.data.repository.query.SecurityEvaluationContextExtension;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
#Configuration
public class MSARepositoryRestMvcConfiguration extends RepositoryRestMvcConfiguration {
private static final Logger LOG = LoggerFactory.getLogger(MSARepositoryRestMvcConfiguration.class);
#Value("${static.path}")
private String staticPath;
// #Bean
// public PasswordEncoder passwordEncoder() {
// return new BCryptPasswordEncoder();
// }
#Override
protected void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.setBasePath("/api");
// config.exposeIdsFor(User.class,Order.class,HeroRating.class,RiderLocation.class,OrderItem.class,Address.class,ShopDetail.class,PromoCode.class,RiderDuty.class,Criteria.class,Setting.class);
config.setReturnBodyForPutAndPost(true);
config.setReturnBodyOnCreate(true);
config.setReturnBodyOnUpdate(true);
}
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
super.addResourceHandlers(registry);
if(staticPath != null) {
LOG.info("Serving static content from " + staticPath);
registry.addResourceHandler("/photos/**").addResourceLocations("file:" + staticPath+"photos/");
registry.addResourceHandler("/").addResourceLocations("classpath:/static/");
}
}
#Bean
public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
return new SecurityEvaluationContextExtension();
}
}
Error
It gives an error on configureRepositoryRestConfiguration to remove override annotation
ErrorMessage
The method configureRepositoryRestConfiguration(RepositoryRestConfiguration) of type MSARepositoryRestMvcConfiguration must override or implement a supertype method
From the current reference documentation, Configuring Spring Data REST:
To customize the configuration, register a RepositoryRestConfigurer (or extend RepositoryRestConfigurerAdapter) and implement or override the configure…-methods relevant to your use case.
SDR configuration outside of RepositoryRestMvcConfiguration was addressed in DATAREST-621 and RepositoryRestConfigurer was introduced in this commit.
According to current version of spring document this method is not available so instead of `configureRepositoryRestConfiguration' we can override following method
#Configuration
public class MSARepositoryRestMvcConfiguration extends RepositoryRestMvcConfiguration {
#Override
public RepositoryRestConfiguration config() {
RepositoryRestConfiguration config = super.config();
config.setBasePath("/api");
config.exposeIdsFor(User.class);
return config;
}
}
Check the current configureRepositoryRestConfiguration definition at Interface RepositoryRestConfigurer.
Example form https://www.baeldung.com/spring-data-rest-serialize-entity-id:
#Configuration
public class RestConfiguration implements RepositoryRestConfigurer {
#Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) {
config.exposeIdsFor(Person.class);
}
}

how to Auto Trim Strings of bean object in spring with Restful api?

I want to trim all the form string fields trim automatically (trailing & leading spaces only)
Suppose if I pass FirstName = " robert "
Expected: "robert"
Controller class having following code :
#InitBinder
public void initBinder ( WebDataBinder binder )
{
StringTrimmerEditor stringtrimmer = new StringTrimmerEditor(true);
binder.registerCustomEditor(String.class, stringtrimmer);
}
#RequestMapping(value = "/createuser", method = RequestMethod.POST)
public Boolean createUser(#RequestBody UserAddUpdateParam userAddUpdateParam) throws Exception {
return userFacade.createUser(userAddUpdateParam);
}
when I debug the code, It's coming into #InitBinder but not trimming bean class string fields.
The annotation #InitBinder doesn't work with #RequestBody, you have to use it with the #ModelAttribute annotation
You can find more information in the Spring documentation:
https://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html
To add this feature to all JSON submitted in post and received in RequestBody you can have following WebMvcConfigurer in place.
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
#Configuration
public class HttpMessageConvertor implements WebMvcConfigurer {
#Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(mappingJackson2HttpMessageConverter());
}
#Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
SimpleModule module = new SimpleModule();
module.addDeserializer(String.class, new StringWithoutSpaceDeserializer(String.class));
mapper.registerModule(module);
converter.setObjectMapper(mapper);
return converter;
}
}
StringWithoutSpaceDeserializer class as :
import java.io.IOException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
public class StringWithoutSpaceDeserializer extends StdDeserializer<String> {
/**
*
*/
private static final long serialVersionUID = -6972065572263950443L;
protected StringWithoutSpaceDeserializer(Class<String> vc) {
super(vc);
}
#Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return p.getText() != null ? p.getText().trim() : null;
}
}
Try out the below code:
#InitBinder
public void setAllowedFields(WebDataBinder dataBinder) {
dataBinder.registerCustomEditor(String.class, new PropertyEditorSupport() {
#Override
public void setAsText(String text) {
if (text == null) {
return;
}
setValue(text);
}
#Override
public String getAsText() {
Object value = getValue();
return (value != null ? value.trim().toString() : "");
}
});
}

Jax-rs json pretty output

in Java when i use the
#Produces("application/json")
annotation the output is not formated into human readable form. How do i achive that?
Just for the record, if you want to enable the pretty output only for some resources you can use the #JacksonFeatures annotation on a resource method.
Here is example:
#Produces(MediaType.APPLICATION_JSON)
#JacksonFeatures(serializationEnable = { SerializationFeature.INDENT_OUTPUT })
public Bean resource() {
return new Bean();
}
This is how you can properly do conditional pretty/non-pretty json output based on presence of "pretty" in query string.
Create a PrettyFilter that implements ContainerResponseFilter, that will be executed on every request:
#Provider
public class PrettyFilter implements ContainerResponseFilter {
#Override
public void filter(ContainerRequestContext reqCtx, ContainerResponseContext respCtx) throws IOException {
UriInfo uriInfo = reqCtx.getUriInfo();
//log.info("prettyFilter: "+uriInfo.getPath());
MultivaluedMap<String, String> queryParameters = uriInfo.getQueryParameters();
if(queryParameters.containsKey("pretty")) {
ObjectWriterInjector.set(new IndentingModifier(true));
}
}
public static class IndentingModifier extends ObjectWriterModifier {
private final boolean indent;
public IndentingModifier(boolean indent) {
this.indent = indent;
}
#Override
public ObjectWriter modify(EndpointConfigBase<?> endpointConfigBase, MultivaluedMap<String, Object> multivaluedMap, Object o, ObjectWriter objectWriter, JsonGenerator jsonGenerator) throws IOException {
if(indent) jsonGenerator.useDefaultPrettyPrinter();
return objectWriter;
}
}
}
And pretty much that's it!
You will need to ensure that this class gets used by Jersey by either automated package scanning or registered manually.
Spent few hours trying to achieve that and found that no-one has published a ready-to-use solution before.
Create this class anywhere in your project. It will be loaded on deployment. Notice the .configure(SerializationConfig.Feature.INDENT_OUTPUT, true); which configures the mapper to format the output.
For Jackson 2.0 and later, replace the two .configure() lines with these:
.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false)
.configure(SerializationFeature.INDENT_OUTPUT, true);
And change your imports accordingly.
package com.secret;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;
import org.codehaus.jackson.map.DeserializationConfig;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
/**
*
* #author secret
*/
#Provider
#Produces(MediaType.APPLICATION_JSON)
public class JacksonContextResolver implements ContextResolver<ObjectMapper> {
private ObjectMapper objectMapper;
public JacksonContextResolver() throws Exception {
this.objectMapper = new ObjectMapper();
this.objectMapper
.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(SerializationConfig.Feature.INDENT_OUTPUT, true);
}
#Override
public ObjectMapper getContext(Class<?> objectType) {
return objectMapper;
}
}
Bear in mind that formatting has a negative effect on performance.
If you are using Spring, then you can globally set the property
spring.jackson.serialization.INDENT_OUTPUT=true
More info at https://docs.spring.io/spring-boot/docs/current/reference/html/howto-properties-and-configuration.html
Building on helpful DaTroop's answer, here is another version which allows choosing between optimized json and formatted json based on the absence or presence of a "pretty" parameter :
package test;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
#Provider
#Produces(MediaType.APPLICATION_JSON)
public class JacksonContextResolver implements ContextResolver<ObjectMapper> {
private ObjectMapper prettyPrintObjectMapper;
private UriInfo uriInfoContext;
public JacksonContextResolver(#Context UriInfo uriInfoContext) throws Exception {
this.uriInfoContext = uriInfoContext;
this.prettyPrintObjectMapper = new ObjectMapper();
this.prettyPrintObjectMapper.configure(SerializationConfig.Feature.INDENT_OUTPUT, true);
}
#Override
public ObjectMapper getContext(Class<?> objectType) {
try {
MultivaluedMap<String, String> queryParameters = uriInfoContext.getQueryParameters();
if(queryParameters.containsKey("pretty")) {
return prettyPrintObjectMapper;
}
} catch(Exception e) {
// protect from invalid access to uriInfoContext.getQueryParameters()
}
return null; // use default mapper
}
}
If you are using the jersey-media-json-binding dependency, which uses Yasson (the official RI of JSR-367) and JAVAX-JSON, you can introduce pretty printing as follows:
import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
import javax.json.bind.JsonbConfig;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;
#Provider
public class RandomConfig implements ContextResolver<Jsonb> {
private final Jsonb jsonb = JsonbBuilder.create(new JsonbConfig().withFormatting(true));
public RandomConfig() { }
#Override
public Jsonb getContext(Class<?> objectType) {
return jsonb;
}
}
Alternative for Jersey 1.x:
org.codehaus.jackson.map.ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationConfig.Feature.INDENT_OUTPUT);

Categories

Resources