I have the following ContextResolver<ObjectMapper> implementation, which based on the query params should return the corresponding JSON mapper (pretty/DateToUtc/Both):
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 com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
#Provider
#Produces(MediaType.APPLICATION_JSON)
public class JsonMapper implements ContextResolver<ObjectMapper> {
private ObjectMapper prettyPrintObjectMapper;
private ObjectMapper dateToUtcMapper;
private ObjectMapper bothMapper;
private UriInfo uriInfoContext;
public JsonMapper(#Context UriInfo uriInfoContext) throws Exception {
this.uriInfoContext = uriInfoContext;
this.prettyPrintObjectMapper = new ObjectMapper();
this.prettyPrintObjectMapper.enable(SerializationFeature.INDENT_OUTPUT);
this.dateToUtcMapper = new ObjectMapper();
this.dateToUtcMapper.enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
this.bothMapper = new ObjectMapper();
this.bothMapper.enable(SerializationFeature.INDENT_OUTPUT);
this.bothMapper.enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
#Override
public ObjectMapper getContext(Class<?> objectType) {
System.out.println("hi");
try {
MultivaluedMap<String, String> queryParameters = uriInfoContext.getQueryParameters();
Boolean containsPretty = queryParameters.containsKey("pretty");
Boolean containsDate = queryParameters.containsKey("date_to_utc");
Boolean containsBoth = containsPretty && containsDate;
if (containsBoth) {
System.out.println("Returning containsBoth");
return bothMapper;
}
if (containsDate) {
System.out.println("Returning containsDate");
return dateToUtcMapper;
}
if (containsPretty) {
System.out.println("Returning pretty");
return prettyPrintObjectMapper;
}
} catch(Exception e) {
// protect from invalid access to uriInfoContext.getQueryParameters()
}
System.out.println("Returning null");
return null; // use default mapper
}
}
And the following Main Application:
private Server configureServer() {
ObjectMapper mapper = new ObjectMapper();
ResourceConfig resourceConfig = new ResourceConfig();
resourceConfig.packages(Calculator.class.getPackage().getName());
resourceConfig.property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true);
// #ValidateOnExecution annotations on subclasses won't cause errors.
resourceConfig.property(ServerProperties.BV_DISABLE_VALIDATE_ON_EXECUTABLE_OVERRIDE_CHECK, true);
resourceConfig.register(JacksonFeature.class);
resourceConfig.register(JsonMapper.class);
resourceConfig.register(AuthFilter.class);
ServletContainer servletContainer = new ServletContainer(resourceConfig);
ServletHolder sh = new ServletHolder(servletContainer);
Server server = new Server(serverPort);
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/");
context.addServlet(sh, "/*");
server.setHandler(context);
return server;
}
However, getContext() function is only called once for the entire server lifetime, only on the first request. The whole idea of this class is to determine on runtime what is the mapper based on the url parameters.
UPDATE
getContext() is called once for each uri path. For example, http://server/path1?pretty=true will yield pretty output for all request to /path1, regardless of thier future pretty queryParam. A call to path2 will call getContext again, but not to future path2 calls.
UPDATE2
Well, it seems like the GetContext is called for each class once, and caches it for that specific class. This is why it expects a class as parameter. So it seems like #LouisF is right, and the objectMapper isn't suited for conditional serialization. However, the ContainerResponseFilter alternative is partially working, but not exposing ObjectMapper features, such as converting dates to UTC. So I'm quite puzzled right now on what is the most appropriate solution for conditional serialization.
SOLVED
With the help of #LoisF, I've managed to have conditional serialization, using ContainerResponseFilter. I havn't use ContextResolver. Below is the working example:
import java.io.IOException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.Provider;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.jaxrs.cfg.EndpointConfigBase;
import com.fasterxml.jackson.jaxrs.cfg.ObjectWriterInjector;
import com.fasterxml.jackson.jaxrs.cfg.ObjectWriterModifier;
/**
* Created by matt on 17/01/2016.
*/
#Provider
public class ResultTransformer implements ContainerResponseFilter {
public static final String OUTPUT_FORMAT_HEADER = "X-Output-Format";
public static final ObjectMapper MAPPER = new ObjectMapper();
public static class OutputFormat {
Boolean pretty = true;
Boolean dateAsTimestamp = false;
public Boolean getPretty() {
return pretty;
}
public void setPretty(Boolean pretty) {
this.pretty = pretty;
}
#JsonProperty("date_as_timestamp")
public Boolean getDateAsTimestamp() {
return dateAsTimestamp;
}
public void setDateAsTimestamp(Boolean dateAsTimestamp) {
this.dateAsTimestamp = dateAsTimestamp;
}
}
#Override
public void filter(ContainerRequestContext reqCtx, ContainerResponseContext respCtx) throws IOException {
String outputFormatStr = reqCtx.getHeaderString(OUTPUT_FORMAT_HEADER);
OutputFormat outputFormat;
if (outputFormatStr == null) {
outputFormat = new OutputFormat();
} else {
try {
outputFormat = MAPPER.readValue(outputFormatStr, OutputFormat.class);
ObjectWriterInjector.set(new IndentingModifier(outputFormat));
} catch (Exception e) {
e.printStackTrace();
ObjectWriterInjector.set(new IndentingModifier(new OutputFormat()));
}
}
}
public static class IndentingModifier extends ObjectWriterModifier {
private OutputFormat outputFormat;
public IndentingModifier(OutputFormat outputFormat) {
this.outputFormat = outputFormat;
}
#Override
public ObjectWriter modify(EndpointConfigBase<?> endpointConfigBase, MultivaluedMap<String, Object> multivaluedMap, Object o, ObjectWriter objectWriter, JsonGenerator jsonGenerator) throws IOException {
if(outputFormat.getPretty()) jsonGenerator.useDefaultPrettyPrinter();
if (outputFormat.dateAsTimestamp) {
objectWriter = objectWriter.with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
} else {
objectWriter = objectWriter.without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
return objectWriter;
}
}
}
You should consider performance.
With your solution, you are creating a new ObjectMapper instance with each request. This is quite heavy!!! I found ObjectMapper creation as main performance stopper during a JProfile measurement.
Not sure if just having 2 static members for pretty / non-pretty is a sufficient solution regarding thread-safety. You need to take care of the mechanism used by the JAX-RS framework in order to cache the ObjectMapper, in order to not have any side-effects.
If you want it by request, you need it to be evaluated for each call. What I would suggest here is to move this logic in a dedicated component and do something as follow :
#GET
public Response demo(#Context final UriInfo uriInfoContext, final String requestBody) {
final ObjectMapper objectMapper = objectMapperResolver.resolve(uriInfoContext.getQueryParameters());
objectMapper.readValue(requestBody, MyClass.class);
...
}
where objectMapperResolver encapsulates the logic of choosing the right ObjectMapper depending on the query parameters
Related
I'm using openapi generator to generate some template code for me, the generated code also include a default Jackson ObjectMapper.
here is the code of the generated provider
package xxx.gen.invoker;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;
import java.io.IOException;
#Provider
public class JacksonConfig implements ContextResolver<ObjectMapper> {
private final ObjectMapper objectMapper;
public JacksonConfig() throws Exception {
objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.setDateFormat(new RFC3339DateFormat());
}
public ObjectMapper getContext(Class<?> arg0) {
return objectMapper;
}
}
Basically, I don't want to modify the generated code, is there any way I can create my own one with higher priority that can override the generated one?
I tried to write my customized one, but it seems that the generated one was picked up during runtime, so what's the resolution progress looks like when JAX RS pick up providers when there're multiple?
here is the code for my own customized provider
package xxx.service.util;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
#Provider
public class ObjectMapperContextResolver implements ContextResolver<ObjectMapper> {
private final ObjectMapper mapper;
public ObjectMapperContextResolver() {
this.mapper = createObjectMapper();
}
#Override
public ObjectMapper getContext(Class<?> type) {
return mapper;
}
private ObjectMapper createObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
return mapper;
}
}
I have a model that I validate with #Valid in my Controllers when requests are send from the front-end:
#NotNull
#Size(min=1, message="Name should be at least 1 character.")
private String name;
#NotNull
#Pattern(regexp = "^https://github.com/.+/.+$", message = "Link to github should match https://github.com/USER/REPOSITORY")
private String github;
but now I am also creating a object with Jackson's ObjectMapper without the controller. Is there a way to register this validation in the ObjectMapper or should I just check the variables in the setters?
You can extend BeanDeserializer and validate object after deserialisation. To register this bean use SimpleModule.
Simple bean deserialiser with validation:
class BeanValidationDeserializer extends BeanDeserializer {
private final static ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
private final Validator validator = factory.getValidator();
public BeanValidationDeserializer(BeanDeserializerBase src) {
super(src);
}
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
Object instance = super.deserialize(p, ctxt);
validate(instance);
return instance;
}
private void validate(Object instance) {
Set<ConstraintViolation<Object>> violations = validator.validate(instance);
if (violations.size() > 0) {
StringBuilder msg = new StringBuilder();
msg.append("JSON object is not valid. Reasons (").append(violations.size()).append("): ");
for (ConstraintViolation<Object> violation : violations) {
msg.append(violation.getMessage()).append(", ");
}
throw new ConstraintViolationException(msg.toString(), violations);
}
}
}
We can use it as below:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.BeanDeserializer;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBase;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.module.SimpleModule;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.io.File;
import java.io.IOException;
import java.util.Set;
public class JsonApp {
public static void main(String[] args) throws Exception {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
SimpleModule validationModule = new SimpleModule();
validationModule.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
if (deserializer instanceof BeanDeserializer) {
return new BeanValidationDeserializer((BeanDeserializer) deserializer);
}
return deserializer;
}
});
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(validationModule);
System.out.println(mapper.readValue(jsonFile, Pojo.class));
}
}
class Pojo {
#NotNull
#Size(min = 1, message = "Name should be at least 1 character.")
private String name;
#NotNull
#Pattern(regexp = "^https://github.com/.+/.+$", message = "Link to github should match https://github.com/USER/REPOSITORY")
private String github;
// getters, setters, toString()
}
For valid JSON payload:
{
"name": "Jackson",
"github": "https://github.com/FasterXML/jackson-databind"
}
prints:
Pojo{name='Jackson', github='https://github.com/FasterXML/jackson-databind'}
For invalid JSON payload:
{
"name": "",
"github": "https://git-hub.com/FasterXML/jackson-databind"
}
prints:
Exception in thread "main" javax.validation.ConstraintViolationException: JSON object is not valid. Reasons (2): Name should be at least 1 character., Link to github should match https://github.com/USER/REPOSITORY,
at BeanValidationDeserializer.validate(JsonApp.java:110)
at BeanValidationDeserializer.deserialize(JsonApp.java:97)
See also:
Java Bean Validation Basics
Deserialize to String or Object using Jackson
Jackson custom serialization and deserialization
I will also post how I managed to do it. Creating class that implements validator:
public class UserValidator implements Validator {
private static final int MINIMUM_NAME_LENGTH = 6;
#Override
public boolean supports(Class<?> clazz) {
return User.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "Name must be at least 7 characters long.");
User foo = (User) target;
if(foo.getGithub().length() > 0 && !extensionSpec.getGithub().matches("^(www|http:|https:)+//github.com/.+/.+$")){
errors.rejectValue("github", "Github must match http://github.com/:user/:repo");
}
if (errors.getFieldErrorCount("name") == 0 && foo.getName().trim().length() < MINIMUM_NAME_LENGTH) {
errors.rejectValue("name", "Name must be at least 7 characters");
}
}
}
Then creating databinder with the deserialized object, taking the binding result and then validating the object:
ObjectMapper mapper = new ObjectMapper();
User foo = mapper.readValue(FooJson, User.class);
Validator validator = new ObjectValidator();
BindingResult bindingResult = new DataBinder(foo).getBindingResult();
validator.validate(foo, bindingResult);
if(bindingResult.hasErrors()){
throw new BindException(bindingResult);
}
Also if you want to take the errorCodes in the body of the response:
#ExceptionHandler
ResponseEntity handleBindException(BindException e){
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(e.getBindingResult().getAllErrors()
.stream()
.map(DefaultMessageSourceResolvable::getCode)
.toArray());
}
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() : "");
}
});
}
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.(●'◡'●)
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);