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.

Programmer DD
Programmer DD
Programmer DD
How to Implement a Non‑Intrusive Unified JSON Response in Spring Boot

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.

backendJavaException HandlingSpring BootresponsebodyadviceJSON response
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.