Non‑Intrusive Unified JSON Response Format with Global Handling in Spring Boot
This article demonstrates how to design a non‑intrusive, unified JSON response structure for Spring Boot applications, defines a standard Result wrapper and status enum, shows static factory methods, and explains global handling via @ResponseResultBody, @RestControllerAdvice, and centralized exception processing.
When a project lacks a consistent API response format, developers often resort to raw HTTP status codes, which can be insufficient for conveying business errors; this tutorial proposes a non‑intrusive solution that wraps all responses in a unified JSON structure.
Standard JSON format :
{
"code": 200,
"message": "OK",
"data": {}
}The code field holds the business status, message provides a description, and data contains the actual payload.
ResultStatus enum defines common status codes and links them to HTTP status values:
@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");
private HttpStatus httpStatus;
private Integer code;
private String message;
ResultStatus(HttpStatus httpStatus, Integer code, String message) {
this.httpStatus = httpStatus;
this.code = code;
this.message = message;
}
}Result wrapper class provides static factory methods for success and failure responses, avoiding the need to instantiate objects manually:
@Getter
@ToString
public class Result
{
private Integer code;
private String message;
private T data;
private Result(ResultStatus resultStatus, T data) {
this.code = resultStatus.getCode();
this.message = resultStatus.getMessage();
this.data = data;
}
public static Result
success() {
return new Result<>(ResultStatus.SUCCESS, null);
}
public static
Result
success(T data) {
return new Result<>(ResultStatus.SUCCESS, data);
}
public static
Result
failure() {
return new Result<>(ResultStatus.INTERNAL_SERVER_ERROR, null);
}
public static
Result
failure(ResultStatus resultStatus) {
return new Result<>(resultStatus, null);
}
public static
Result
failure(ResultStatus resultStatus, T data) {
return new Result<>(resultStatus, data);
}
}Using the wrapper in a controller becomes straightforward:
@RestController
@RequestMapping("/hello")
public class HelloController {
private static final HashMap
INFO;
static {
INFO = new HashMap<>();
INFO.put("name", "galaxy");
INFO.put("age", "70");
}
@GetMapping("/hello")
public Map
hello() {
return INFO;
}
@GetMapping("/result")
@ResponseBody
public Result
> helloResult() {
return Result.success(INFO);
}
}To avoid adding Result<T> manually to every endpoint, a custom annotation @ResponseResultBody and a ResponseBodyAdvice implementation are introduced:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@ResponseBody
public @interface ResponseResultBody {}
@RestControllerAdvice
public class ResponseResultBodyAdvice implements ResponseBodyAdvice
{
private static final Class
ANNOTATION_TYPE = ResponseResultBody.class;
@Override
public boolean supports(MethodParameter returnType, Class
> converterType) {
return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ANNOTATION_TYPE)
|| returnType.hasMethodAnnotation(ANNOTATION_TYPE);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class
> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Result) {
return body;
}
return Result.success(body);
}
}Controllers annotated with @ResponseResultBody can now return plain objects, and the advice automatically wraps them:
@RestController
@RequestMapping("/helloResult")
@ResponseResultBody
public class HelloResultController {
private static final HashMap
INFO;
static {
INFO = new HashMap<>();
INFO.put("name", "galaxy");
INFO.put("age", "70");
}
@GetMapping("hello")
public HashMap
hello() {
return INFO;
}
@GetMapping("result")
public Result
> helloResult() {
return Result.success(INFO);
}
@GetMapping("helloError")
public HashMap
helloError() throws Exception {
throw new Exception("helloError");
}
@GetMapping("helloMyError")
public HashMap
helloMyError() throws Exception {
throw new ResultException();
}
}For unified exception handling, a global @RestControllerAdvice is added that catches all exceptions, distinguishes custom ResultException from generic ones, and returns a Result with appropriate status:
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler implements ResponseBodyAdvice
{
private static final Class
ANNOTATION_TYPE = ResponseResultBody.class;
@Override
public boolean supports(MethodParameter returnType, Class
> converterType) {
return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ANNOTATION_TYPE)
|| returnType.hasMethodAnnotation(ANNOTATION_TYPE);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class
> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Result) {
return body;
}
return Result.success(body);
}
@ExceptionHandler(Exception.class)
public final ResponseEntity
> 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
> 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
> 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
> 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);
}
}The article also briefly mentions older approaches such as using @ResponseStatus on controllers or exceptions, and a low‑level HandlerExceptionResolver that writes JSON directly via PrintWriter , pointing out their drawbacks compared to the annotation‑driven solution.
Finally, the post includes several reference links and promotional material for a WeChat architecture community, but the core technical content remains a practical guide for backend developers seeking a clean, global JSON response strategy.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
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.