Backend Development 10 min read

How to Use Spring Boot’s ResponseBodyAdvice for Unified API Responses

This tutorial explains how to create a global response wrapper in Spring Boot 3.2.5 using ResponseBodyAdvice, covering the pros and cons of controller‑level wrapping, custom annotations, code examples, handling of String responses, and testing the unified output with sample requests.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How to Use Spring Boot’s ResponseBodyAdvice for Unified API Responses

Environment: Spring Boot 3.2.5

1. Introduction

When building a RESTful API with Spring Boot, developers often define a unified response object to ensure consistent and readable results for both success and failure cases. The simplest approach is to wrap data directly in the controller, which has obvious advantages and drawbacks.

Advantages

Intuitive / Simple : Controllers handle packaging directly, requiring no extra configuration or interceptors.

Flexibility : Controllers can add extra fields or perform business‑specific operations before returning data.

Easy to Test : Because the packaging logic resides in the controller, unit tests can target the controller without involving other layers.

Disadvantages

Code Redundancy : Every endpoint must return the same wrapper, leading to repetitive code.

Maintenance Cost : Adding or modifying response fields requires changes in all affected controllers.

To avoid these issues, Spring provides the ResponseBodyAdvice interface, which allows global manipulation of response bodies.

2. Practical Example

2.1 Define a Unified Result Class

<code>public class R<T> {
    /** 0‑success, 1‑failure */
    private Integer code = 0;
    private String msg = "成功";
    private T data;

    public static <T> R<T> success(T data) {
        R<T> r = new R<>();
        r.setData(data);
        return r;
    }

    public static <T> R<T> success(String msg) {
        R<T> r = new R<>();
        r.setMsg(msg);
        return r;
    }
    // getters, setters
}
</code>

This generic class can hold any type of payload.

2.2 Define the Controller

<code>@RestController
@RequestMapping("/users")
public class UserController {
    // Mock data
    private static final List<User> DATAS = List.of(
        new User(1L, "张三"),
        new User(2L, "李四"),
        new User(3L, "王五"),
        new User(4L, "赵六")
    );

    @GetMapping
    public List<User> list() {
        return DATAS;
    }

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
        return DATAS.stream().filter(u -> u.getId().equals(id)).findFirst().orElse(null);
    }

    @GetMapping("/name")
    public R<User> queryByName(String name) {
        return R.success(DATAS.stream().filter(u -> u.getName().equals(name)).findFirst().orElse(null));
    }

    @NoWrapper
    @GetMapping("/odd")
    public List<User> all() {
        return DATAS.stream().filter(u -> u.getId() % 2 > 0).toList();
    }

    @PostMapping
    public void save(@RequestBody User user) {
        System.out.println("保存成功");
    }
}
</code>

Three simple endpoints are defined for demonstration.

2.3 Define a Custom Annotation

<code>@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface NoWrapper {}
</code>

When this annotation is present, the response will not be wrapped.

2.4 Implement ResponseBodyAdvice

<code>@ControllerAdvice
public class PackResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        Class<?> clazz = returnType.getDeclaringClass();
        // Do not wrap if:
        // 1. Class is annotated with @NoWrapper
        // 2. Method is annotated with @NoWrapper
        // 3. Return type is already R
        return !clazz.isAnnotationPresent(NoWrapper.class)
                && !returnType.hasMethodAnnotation(NoWrapper.class)
                && !R.class.isAssignableFrom(returnType.getParameterType());
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof String str) {
            // Ensure String responses keep the same structure
            return this.objectMapper.writeValueAsString(R.success(str));
        }
        return R.success(body);
    }
}
</code>

The beforeBodyWrite method wraps non‑R results into an R instance. Special handling for String avoids type‑conversion errors.

3. Testing the Endpoints

Sample requests and their JSON responses (illustrated with screenshots in the original article):

GET /users – returns a list of users.

GET /users/2 – returns the user with ID 2.

GET /users/name?name=赵六 – returns an R object containing the matching user.

POST /users – no response body (void).

GET /users/odd – returns only odd‑ID users; the method is annotated with @NoWrapper , so the result is not wrapped.

4. Important Considerations

When a controller method returns a String , Spring uses StringHttpMessageConverter to write the response. If ResponseBodyAdvice wraps the String into an R object, a type‑mismatch occurs because the converter expects a String . The fix is to detect String bodies and manually serialize the wrapped R to JSON, as shown in the beforeBodyWrite implementation above.

After applying this fix, all endpoints return a consistent JSON structure.

5. Conclusion

Using ResponseBodyAdvice provides a clean, centralized way to enforce a unified response format in Spring Boot applications, while custom annotations like @NoWrapper give fine‑grained control over which endpoints should bypass the wrapper.

backendJavaSpring BootResponseBodyAdviceAPI Wrapper
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

login 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.