How to enable dynamic beautiful json print based on http request header in Spring MVC?

I want to pretty quickly debug json responses from Spring MVC Restcontrollers dynamically based on the http parameter (e.g. here: http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api#pretty-print-gzip )

I found configurations for pretty static printing of its configuration, but not how to do it dynamically?

When using Spring MVC for REST, how do you let Jackson print beautifully rendered JSON?

Any idea how to do this?

+5
source share
4 answers

Introducing a New Media Type


You can define a new media type, say application/pretty+json and register a new HttpMessageConverter that will convert to this media type. In fact, if the client sends a request with the heading Accept: application/pretty+json , our new HttpMessageConverter will write the answer, otherwise it can be done by the plain old MappingJackson2HttpMessageConverter .

So, extends MappingJackson2HttpMessageConverter as follows:

 public class PrettyPrintJsonConverter extends MappingJackson2HttpMessageConverter { public PrettyPrintJsonConverter() { setPrettyPrint(true); } @Override public List<MediaType> getSupportedMediaTypes() { return Collections.singletonList(new MediaType("application", "pretty+json")); } @Override public boolean canWrite(Class<?> clazz, MediaType mediaType) { boolean canWrite = super.canWrite(clazz, mediaType); boolean canWritePrettily = mediaType != null && mediaType.getSubtype().equals("pretty+json"); return canWrite && canWritePrettily; } } 

That setPrettyPrint(true) in the constructor will do the trick for us. Then we have to register this HttpMessageConverter :

 @EnableWebMvc @Configuration public class WebConfig extends WebMvcConfigurerAdapter { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(new PrettyPrintJsonConverter()); } } 

As I said, if the client sends a request with the header application/pretty+json Accept, our PrettyPrintJsonConverter will write the JSON Prettily view. Otherwise, MappingJackson2HttpMessageConverter writes compact JSON to the response body.

You can achieve the same with ResponseBodyAdvice or even with interceptors, but in my opinion, the new HttpMessageConverter best suited.

+4
source

To switch to pretty rendering with a parameter? pretty = true, I am using custom MappingJackson2HttpMessageConverter

 @Configuration @RestController public class MyController { @Bean MappingJackson2HttpMessageConverter currentMappingJackson2HttpMessageConverter() { MappingJackson2HttpMessageConverter jsonConverter = new CustomMappingJackson2HttpMessageConverter(); return jsonConverter; } public static class Input { public String pretty; } public static class Output { @JsonIgnore public String pretty; } @RequestMapping(path = "/api/test", method = {RequestMethod.GET, RequestMethod.POST}) Output test( @RequestBody(required = false) Input input, @RequestParam(required = false, value = "pretty") String pretty) { if (input.pretty==null) input.pretty = pretty; Output output = new Output(); output.pretty = input.pretty; return output; } } 

Converter:

 public class CustomMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter { ObjectMapper objectMapper; ObjectMapper prettyPrintObjectMapper; public CustomMappingJackson2HttpMessageConverter() { objectMapper = new ObjectMapper(); prettyPrintObjectMapper = new ObjectMapper(); prettyPrintObjectMapper.configure(SerializationFeature.INDENT_OUTPUT, true); } @Override @SuppressWarnings("deprecation") protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType()); JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding); try { writePrefix(generator, object); Class<?> serializationView = null; FilterProvider filters = null; Object value = object; JavaType javaType = null; if (object instanceof MappingJacksonValue) { MappingJacksonValue container = (MappingJacksonValue) object; value = container.getValue(); serializationView = container.getSerializationView(); filters = container.getFilters(); } javaType = getJavaType(type, null); ObjectMapper currentMapper = objectMapper; Field prettyField = ReflectionUtils.findField(object.getClass(), "pretty"); if (prettyField != null) { Object prettyObject = ReflectionUtils.getField(prettyField, object); if (prettyObject != null && prettyObject instanceof String) { String pretty = (String)prettyObject; if (pretty.equals("true")) currentMapper = prettyPrintObjectMapper; } } ObjectWriter objectWriter; if (serializationView != null) { objectWriter = currentMapper.writerWithView(serializationView); } else if (filters != null) { objectWriter = currentMapper.writer(filters); } else { objectWriter = currentMapper.writer(); } if (javaType != null && javaType.isContainerType()) { objectWriter = objectWriter.withType(javaType); } objectWriter.writeValue(generator, value); writeSuffix(generator, object); generator.flush(); } catch (JsonProcessingException ex) { throw new HttpMessageNotWritableException("Could not write content: " + ex.getMessage(), ex); } } } 

Franc

+1
source

I like the approach to Franck Lefebure, but I don't like to use reflection, so here is a solution using custom PrettyFormattedBody types + pretty formatted arrays / lists

Spring Configuration:

 @Bean MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() { return new CustomJsonResponseMapper(); } 

CustomJsonResponseMapper.java:

 public class CustomJsonResponseMapper extends MappingJackson2HttpMessageConverter { private final ObjectMapper prettyPrintObjectMapper; public CustomJsonResponseMapper() { super(); prettyPrintObjectMapper = initiatePrettyObjectMapper(); } protected ObjectMapper initiatePrettyObjectMapper() { // clone and re-configure default object mapper final ObjectMapper prettyObjectMapper = objectMapper != null ? objectMapper.copy() : new ObjectMapper(); prettyObjectMapper.configure(SerializationFeature.INDENT_OUTPUT, true); // for arrays - use new line for every entry DefaultPrettyPrinter pp = new DefaultPrettyPrinter(); pp.indentArraysWith(new DefaultIndenter()); prettyObjectMapper.setDefaultPrettyPrinter(pp); return prettyObjectMapper; } @Override protected void writeInternal(final Object objectToWrite, final Type type, final HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { // based on: if objectToWrite is PrettyFormattedBody with isPretty == true => use custom formatter // otherwise - use the default one final Optional<PrettyFormattedBody> prettyFormatted = Optional.ofNullable(objectToWrite) .filter(o -> o instanceof PrettyFormattedBody) .map(o -> (PrettyFormattedBody) objectToWrite); final boolean pretty = prettyFormatted.map(PrettyFormattedBody::isPretty).orElse(false); final Object realObject = prettyFormatted.map(PrettyFormattedBody::getBody).orElse(objectToWrite); if (pretty) { // this is basically full copy of super.writeInternal(), but with custom (pretty) object mapper MediaType contentType = outputMessage.getHeaders().getContentType(); JsonEncoding encoding = getJsonEncoding(contentType); JsonGenerator generator = this.prettyPrintObjectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding); try { writePrefix(generator, realObject); Class<?> serializationView = null; FilterProvider filters = null; Object value = realObject; JavaType javaType = null; if (realObject instanceof MappingJacksonValue) { MappingJacksonValue container = (MappingJacksonValue) realObject; value = container.getValue(); serializationView = container.getSerializationView(); filters = container.getFilters(); } if (type != null && value != null && TypeUtils.isAssignable(type, value.getClass())) { javaType = getJavaType(type, null); } ObjectWriter objectWriter; if (serializationView != null) { objectWriter = this.prettyPrintObjectMapper.writerWithView(serializationView); } else if (filters != null) { objectWriter = this.prettyPrintObjectMapper.writer(filters); } else { objectWriter = this.prettyPrintObjectMapper.writer(); } if (javaType != null && javaType.isContainerType()) { objectWriter = objectWriter.forType(javaType); } objectWriter.writeValue(generator, value); writeSuffix(generator, realObject); generator.flush(); } catch (JsonProcessingException ex) { throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex); } } else { // use default formatting if isPretty property is not specified super.writeInternal(realObject, type, outputMessage); } } @Override public boolean canWrite(Class<?> clazz, MediaType mediaType) { // this should be mandatory overridden, // otherwise writeInternal() won't be called with custom PrettyFormattedBody type return (PrettyFormattedBody.class.equals(clazz) && canWrite(mediaType)) || super.canWrite(clazz, mediaType); } public static final class PrettyFormattedBody { private final Object body; private final boolean pretty; public PrettyFormattedBody(Object body, boolean pretty) { this.body = body; this.pretty = pretty; } public Object getBody() { return body; } public boolean isPretty() { return pretty; } } } 

HealthController.java (a rather optional request parameter):

 @RequestMapping(value = {"/", "/health"}, produces = APPLICATION_JSON_VALUE) public ResponseEntity<?> health(@RequestParam Optional<String> pretty) { return new ResponseEntity<>( new CustomJsonResponseMapper.PrettyFormattedBody(healthResult(), pretty.isPresent()), HttpStatus.OK); } 

Example response http://localhost:8080 :

 {"status":"OK","statusCode":200,"endpoints":["/aaa","/bbb","/ccc"]} 

Response example http://localhost:8080?pretty :

 { "status": "OK", "statusCode": 200, "endpoints": [ "/aaa", "/bbb", "/ccc" ] } 
0
source

Another solution if using the Gson formatter ( link to the full request request ):

Spring Configuration (definition of 2 beans):

 @Bean public Gson gson() { return new GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.IDENTITY) .disableHtmlEscaping() .create(); } /** * @return same as {@link #gson()}, but with <code>{@link Gson#prettyPrinting} == true</code>, eg use indentation */ @Bean public Gson prettyGson() { return new GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.IDENTITY) .setPrettyPrinting() .disableHtmlEscaping() .create(); } /** * Custom JSON objects mapper: uses {@link #gson()} as a default JSON HTTP request/response mapper * and {@link #prettyGson()} as mapper for pretty-printed JSON objects. See {@link PrettyGsonMessageConverter} for * how pretty print is requested. * <p> * <b>Note:</b> {@link FieldNamingPolicy#IDENTITY} field mapping policy is important at least for * {@link PaymentHandleResponse#getPayment()} method. See respective documentation for details. * * @return default HTTP request/response mapper, based on {@link #gson()} bean. */ @Bean public GsonHttpMessageConverter gsonMessageConverter() { return new PrettyGsonMessageConverter(gson(), prettyGson()); } 

PrettyGsonMessageConverter.java:

 /** * Custom Gson response message converter to allow JSON pretty print, if requested. * <p> * The class extends default Spring {@link GsonHttpMessageConverter} adding {@link #prettyGson} mapper and processing * {@link PrettyFormattedBody} instances. */ public class PrettyGsonMessageConverter extends GsonHttpMessageConverter { /** * JSON message converter with configured pretty print options, which is used when a response is expected to be * pretty printed. */ private final Gson prettyGson; /** * @see GsonHttpMessageConverter#jsonPrefix */ private String jsonPrefix; /** * @param gson default (minified) JSON mapper. This value is set to {@code super.gson} property. * @param prettyGson pretty configure JSON mapper, which is used if the body expected to be pretty printed */ public PrettyGsonMessageConverter(final Gson gson, final Gson prettyGson) { super(); this.setGson(gson); this.prettyGson = prettyGson; } /** * Because base {@link GsonHttpMessageConverter#jsonPrefix} is private, but is used in overloaded * {@link #writeInternal(Object, Type, HttpOutputMessage)} - we should copy this value. * * @see GsonHttpMessageConverter#setJsonPrefix(String) */ @Override public void setJsonPrefix(String jsonPrefix) { super.setJsonPrefix(jsonPrefix); this.jsonPrefix = jsonPrefix; } /** * Because base {@link GsonHttpMessageConverter#jsonPrefix} is private, but is used in overloaded * {@link #writeInternal(Object, Type, HttpOutputMessage)} - we should copy this value. * * @see GsonHttpMessageConverter#setPrefixJson(boolean) */ @Override public void setPrefixJson(boolean prefixJson) { super.setPrefixJson(prefixJson); this.jsonPrefix = (prefixJson ? ")]}', " : null); } /** * Allow response JSON pretty print if {@code objectToWrite} is a {@link PrettyFormattedBody} instance with * <code>{@link PrettyFormattedBody#isPretty() isPretty} == true</code>. * * @param objectToWrite if the value is {@link PrettyFormattedBody} instance with * <code>{@link PrettyFormattedBody#isPretty() isPretty} == true</code> - use * {@link #prettyGson} for output writing. Otherwise use base * {@link GsonHttpMessageConverter#writeInternal(Object, Type, HttpOutputMessage)} * @param type the type of object to write (may be {@code null}) * @param outputMessage the HTTP output message to write to * @throws IOException in case of I/O errors * @throws HttpMessageNotWritableException in case of conversion errors */ @Override protected void writeInternal(@Nullable final Object objectToWrite, @Nullable final Type type, @Nonnull final HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { // based on: if objectToWrite is PrettyFormattedBody && isPretty == true => use custom formatter // otherwise - use the default base GsonHttpMessageConverter#writeInternal(Object, Type, HttpOutputMessage) Optional<PrettyFormattedBody> prettyFormatted = Optional.ofNullable(objectToWrite) .filter(o -> o instanceof PrettyFormattedBody) .map(o -> (PrettyFormattedBody) objectToWrite); boolean pretty = prettyFormatted.map(PrettyFormattedBody::isPretty).orElse(false); Object realObject = prettyFormatted.map(PrettyFormattedBody::getBody).orElse(objectToWrite); if (pretty) { // this is basically full copy of super.writeInternal(), but with custom (pretty) gson mapper Charset charset = getCharset(outputMessage.getHeaders()); OutputStreamWriter writer = new OutputStreamWriter(outputMessage.getBody(), charset); try { if (this.jsonPrefix != null) { writer.append(this.jsonPrefix); } if (type != null) { this.prettyGson.toJson(realObject, type, writer); } else { this.prettyGson.toJson(realObject, writer); } writer.close(); } catch (JsonIOException ex) { throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex); } } else { // use default writer if isPretty property is not specified super.writeInternal(realObject, type, outputMessage); } } /** * To ensure the message converter supports {@link PrettyFormattedBody} instances * * @param clazz response body class * @return <b>true</b> if the {@code clazz} is {@link PrettyFormattedBody} or {@code super.supports(clazz) == true} */ @Override protected boolean supports(Class<?> clazz) { return PrettyFormattedBody.class.equals(clazz) || super.supports(clazz); } /** * Just a copy-paste of {@link GsonHttpMessageConverter#getCharset(HttpHeaders)} because it is private, but used in * {@link #writeInternal(Object, Type, HttpOutputMessage)} * * @param headers output message HTTP headers * @return a charset from the {@code headers} content type or {@link GsonHttpMessageConverter#DEFAULT_CHARSET} * otherwise. */ private Charset getCharset(HttpHeaders headers) { if (headers == null || headers.getContentType() == null || headers.getContentType().getCharset() == null) { return DEFAULT_CHARSET; } return headers.getContentType().getCharset(); } } 

PrettyFormattedBody.java:

 public final class PrettyFormattedBody { private final Object body; private final boolean pretty; private PrettyFormattedBody(@Nonnull final Object body, final boolean pretty) { this.body = body; this.pretty = pretty; } public Object getBody() { return body; } public boolean isPretty() { return pretty; } public static PrettyFormattedBody of(@Nonnull final Object body, final boolean pretty) { return new PrettyFormattedBody(body, pretty); } } 

and finally, the controller itself:

  @RequestMapping( value = {"/health", "/"}, produces = APPLICATION_JSON_VALUE) public ResponseEntity<?> checkHealth(@RequestParam(required = false) String pretty, @Autowired ApplicationInfo applicationInfo) { Map<String, Object> tenantResponse = new HashMap<>(); tenantResponse.put(APP_INFO_KEY, applicationInfo); return new ResponseEntity<>(PrettyFormattedBody.of(tenantResponse, pretty != null), HttpStatus.OK); } 
0
source

Source: https://habr.com/ru/post/1247278/


All Articles