How to Implement a Non‑Intrusive Unified JSON Response in Spring Boot
This tutorial explains why a unified JSON format is needed in a Spring Boot project, defines a standard {code, message, data} schema, shows how to create a Result enum and generic wrapper class, and demonstrates global handling with @ResponseResultBody, ResponseBodyAdvice, and @ExceptionHandler for clean, consistent API responses.
Non‑Intrusive Unified JSON Response
Although the original project did not provide a unified API response format, the author introduced a non‑intrusive solution that avoids modifying existing interfaces while ensuring consistent JSON output.
Project source code: https://github.com/469753862/galaxy-blogs/tree/master/code/responseResult
Define JSON Format
Define Return JSON Structure
Backend services usually return JSON with the following fields:
{
"code": 200,
"message": "OK",
"data": {}
}code: return status code
message: description of the result
data: actual payload
Define JavaBean Fields
Define Status Code Enum
@ToString
@Getter
public enum ResultStatus {
SUCCESS(HttpStatus.OK, 200, "OK"),
BAD_REQUEST(HttpStatus.BAD_REQUEST, 400, "Bad Request"),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 500, "Internal Server Error");
/** HTTP status code */
private HttpStatus httpStatus;
/** Business error code */
private Integer code;
/** Business error message */
private String message;
ResultStatus(HttpStatus httpStatus, Integer code, String message) {
this.httpStatus = httpStatus;
this.code = code;
this.message = message;
}
}The enum maps business error codes to HTTP status codes for easier maintenance. If you prefer not to use HTTP status codes, you can remove the HttpStatus field.
Define Response Body Class
@Getter
@ToString
public class Result<T> {
/** Business error code */
private Integer code;
/** Message description */
private String message;
/** Return data */
private T data;
private Result(ResultStatus resultStatus, T data) {
this.code = resultStatus.getCode();
this.message = resultStatus.getMessage();
this.data = data;
}
/** Success without data */
public static Result<Void> success() {
return new Result<>(ResultStatus.SUCCESS, null);
}
/** Success with data */
public static <T> Result<T> success(T data) {
return new Result<>(ResultStatus.SUCCESS, data);
}
/** Success with custom status */
public static <T> Result<T> success(ResultStatus resultStatus, T data) {
if (resultStatus == null) {
return success(data);
}
return new Result<>(resultStatus, data);
}
/** Failure without data */
public static <T> Result<T> failure() {
return new Result<>(ResultStatus.INTERNAL_SERVER_ERROR, null);
}
/** Failure with custom status */
public static <T> Result<T> failure(ResultStatus resultStatus) {
return failure(resultStatus, null);
}
/** Failure with custom status and data */
public static <T> Result<T> failure(ResultStatus resultStatus, T data) {
if (resultStatus == null) {
return new Result<>(ResultStatus.INTERNAL_SERVER_ERROR, null);
}
return new Result<>(resultStatus, data);
}
}Static factory methods make object creation concise and clear.
Result Entity Test
@RestController
@RequestMapping("/hello")
public class HelloController {
private static final HashMap<String, Object> INFO;
static {
INFO = new HashMap<>();
INFO.put("name", "galaxy");
INFO.put("age", "70");
}
@GetMapping("/hello")
public Map<String, Object> hello() {
return INFO;
}
@GetMapping("/result")
@ResponseBody
public Result<Map<String, Object>> helloResult() {
return Result.success(INFO);
}
}At this point the basic unified JSON format works, but returning Result<Object> for every endpoint feels repetitive.
Advanced Unified JSON Response – Global Handling (@RestControllerAdvice)
Using @ResponseBody automatically serializes objects to JSON. By intercepting the response before serialization, we can wrap any object into Result<Object> without changing existing controllers.
@ResponseBody Inheritance Annotation
Define a custom annotation that can be placed on classes or methods.
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@ResponseBody
public @interface ResponseResultBody {
}ResponseBodyAdvice Implementation
@RestControllerAdvice
public class ResponseResultBodyAdvice implements ResponseBodyAdvice<Object> {
private static final Class<? extends Annotation> ANNOTATION_TYPE = ResponseResultBody.class;
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ANNOTATION_TYPE)
|| returnType.hasMethodAnnotation(ANNOTATION_TYPE);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Result) {
return body;
}
return Result.success(body);
}
}RestControllerAdvice Test
@RestController
@RequestMapping("/helloResult")
@ResponseResultBody
public class HelloResultController {
private static final HashMap<String, Object> INFO;
static {
INFO = new HashMap<>();
INFO.put("name", "galaxy");
INFO.put("age", "70");
}
@GetMapping("hello")
public HashMap<String, Object> hello() {
return INFO;
}
/** Test duplicate wrapping */
@GetMapping("result")
public Result<Map<String, Object>> helloResult() {
return Result.success(INFO);
}
@GetMapping("helloError")
public HashMap<String, Object> helloError() throws Exception {
throw new Exception("helloError");
}
@GetMapping("helloMyError")
public HashMap<String, Object> helloMyError() throws Exception {
throw new ResultException();
}
}Now any method annotated with @ResponseResultBody can return a plain object and it will be automatically wrapped into the unified JSON structure.
Advanced Unified JSON Response – Exception Handling (@ExceptionHandler)
Exception handling can also be centralized. The following sections show a discouraged @ResponseStatus approach and a recommended @ExceptionHandler implementation.
Exception Handling with @ResponseStatus (Not Recommended)
@RestController
@RequestMapping("/error")
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR, reason = "Java exception")
public class HelloExceptionController {
private static final HashMap<String, Object> INFO;
static {
INFO = new HashMap<>();
INFO.put("name", "galaxy");
INFO.put("age", "70");
}
@GetMapping
public HashMap<String, Object> helloError() throws Exception {
throw new Exception("helloError");
}
@GetMapping("helloJavaError")
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR, reason = "Java exception")
public HashMap<String, Object> helloJavaError() throws Exception {
throw new Exception("helloError");
}
@GetMapping("helloMyError")
public HashMap<String, Object> helloMyError() throws Exception {
throw new MyException();
}
}
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR, reason = "Custom exception")
class MyException extends Exception {}Global Exception Handling with @ExceptionHandler (Recommended)
The previous ResponseResultBodyAdvice is enhanced to handle exceptions uniformly.
@Slf4j
@RestControllerAdvice
public class ResponseResultBodyAdvice implements ResponseBodyAdvice<Object> {
private static final Class<? extends Annotation> ANNOTATION_TYPE = ResponseResultBody.class;
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ANNOTATION_TYPE)
|| returnType.hasMethodAnnotation(ANNOTATION_TYPE);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Result) {
return body;
}
return Result.success(body);
}
@ExceptionHandler(Exception.class)
public final ResponseEntity<Result<?>> exceptionHandler(Exception ex, WebRequest request) {
log.error("ExceptionHandler: {}", ex.getMessage());
HttpHeaders headers = new HttpHeaders();
if (ex instanceof ResultException) {
return handleResultException((ResultException) ex, headers, request);
}
return handleException(ex, headers, request);
}
protected ResponseEntity<Result<?>> handleResultException(ResultException ex, HttpHeaders headers, WebRequest request) {
Result<?> body = Result.failure(ex.getResultStatus());
HttpStatus status = ex.getResultStatus().getHttpStatus();
return handleExceptionInternal(ex, body, headers, status, request);
}
protected ResponseEntity<Result<?>> handleException(Exception ex, HttpHeaders headers, WebRequest request) {
Result<?> body = Result.failure();
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
return handleExceptionInternal(ex, body, headers, status, request);
}
protected ResponseEntity<Result<?>> handleExceptionInternal(Exception ex, Result<?> body,
HttpHeaders headers, HttpStatus status,
WebRequest request) {
if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) {
request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST);
}
return new ResponseEntity<>(body, headers, status);
}
}This approach provides a clean, centralized way to return a consistent JSON structure for both normal responses and error conditions.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
