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<T> {
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<Void> success() {
return new Result<>(ResultStatus.SUCCESS, null);
}
public static <T> Result<T> success(T data) {
return new Result<>(ResultStatus.SUCCESS, data);
}
public static <T> Result<T> failure() {
return new Result<>(ResultStatus.INTERNAL_SERVER_ERROR, null);
}
public static <T> Result<T> failure(ResultStatus resultStatus) {
return new Result<>(resultStatus, null);
}
public static <T> Result<T> 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<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);
}
}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<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);
}
}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<String, Object> INFO;
static {
INFO = new HashMap<>();
INFO.put("name", "galaxy");
INFO.put("age", "70");
}
@GetMapping("hello")
public HashMap<String, Object> hello() {
return INFO;
}
@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();
}
}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<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);
}
}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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
