Spring Boot

ModelAndView

ModelAndView is a Spring MVC holder that combines a logical view name and model data in a single return value. It is an alternative to returning a String view name and populating a separate Model parameter — useful when the view name is determined dynamically, when returning from exception handlers, or when working with legacy code.

How ModelAndView Works in Spring MVC

In a server-rendered Spring MVC application, every request that returns an HTML page ends with two pieces of information: which view to render and which data that view needs. ModelAndView (org.springframework.web.servlet.ModelAndView) is a small holder class that carries both. The flow is short. DispatcherServlet receives the request, finds the matching controller method, and calls it. If the method returns a ModelAndView, Spring reads the view name and the model from it, asks a ViewResolver to turn the name into a real view (a Thymeleaf template or a JSP, for example), and renders that view with the model. Returning a String view name plus a Model parameter goes through the same steps, because Spring converts both forms into the same internal pair of view name and model before rendering. A ModelAndView holds three things you will set most often: a view name, model attributes added with addObject(), and optionally an HTTP status set with setStatus().

ModelAndView Basics

ModelAndView bundles the logical view name and the model attributes into one object. A handler method returns it instead of a String view name, and Spring MVC hands the model to the ViewResolver and renders the named view. The result is identical to returning a String and filling a Model parameter separately. The examples below show the ways you will usually build one. The single-argument constructor takes just a view name, and you add attributes afterwards with addObject(name, value). A three-argument constructor takes the view name, one attribute name and its value, which is handy for simple pages. You can call addObject() several times for pages that need more data, and choose the view name from a variable when it depends on a request parameter or a user setting. An addObject(value) overload without a name also exists. Spring then generates the attribute name from the value's type, and for a List of User objects that name is userList, not users. Passing an explicit name is clearer.
Java
@Controller
@RequestMapping("/users")
public class UserController {

    // ── String + Model (preferred for simple cases): ──────────────────
    @GetMapping("/{id}")
    public String findById(@PathVariable Long id, Model model) {
        model.addAttribute("user", userService.findById(id));
        return "users/detail";
    }

    // ── ModelAndView (equivalent result): ─────────────────────────────
    @GetMapping("/{id}/mav")
    public ModelAndView findByIdMav(@PathVariable Long id) {
        ModelAndView mav = new ModelAndView("users/detail");
        mav.addObject("user", userService.findById(id));
        return mav;
    }

    // ── Constructor shorthand — view name + one attribute: ────────────
    @GetMapping("/list")
    public ModelAndView list() {
        return new ModelAndView("users/list",
            "users", userService.findAll());
        // "users/list" = view name, "users" = attribute name, value = list
    }

    // ── Multiple attributes: ───────────────────────────────────────────
    @GetMapping("/dashboard")
    public ModelAndView dashboard() {
        ModelAndView mav = new ModelAndView("users/dashboard");
        mav.addObject("stats",        userService.getStats());
        mav.addObject("recentUsers",  userService.findRecent(5));
        mav.addObject("activeCount",  userService.countActive());
        return mav;
    }

    // ── Dynamic view name — chosen at runtime: ────────────────────────
    @GetMapping("/{id}/view")
    public ModelAndView dynamicView(
            @PathVariable Long id,
            @RequestParam(defaultValue = "detail") String layout) {

        UserResponse user = userService.findById(id);
        String viewName = switch (layout) {
            case "compact" -> "users/compact";
            case "print"   -> "users/print";
            default        -> "users/detail";
        };
        return new ModelAndView(viewName, "user", user);
    }
}

ModelAndView with Redirect and Forward

ModelAndView accepts the same redirect: and forward: prefixes as String return values. Use redirect: after a successful form submission so the browser does not resubmit the form when the user refreshes the page (the Post/Redirect/Get pattern). Be careful with attributes on a redirect view: simple values you add may be appended to the redirect URL as query parameters, so never put sensitive data there. To carry a one-time message across a redirect, use RedirectAttributes instead. Two other features are worth knowing. setStatus() lets you pick the HTTP status per call, for example 404 or 410 on an HTML error page, without touching HttpServletResponse. And if a handler writes to HttpServletResponse itself, it can return null; Spring then treats the request as already handled and skips view rendering. The error-handler example at the bottom shows the most natural use of ModelAndView in modern code: a @ControllerAdvice that renders an HTML error page with a specific status. If your application returns JSON, use @RestControllerAdvice with ResponseEntity instead.
Java
@Controller
@RequestMapping("/users")
public class UserController {

    // ── Redirect: ─────────────────────────────────────────────────────
    @PostMapping
    public ModelAndView create(
            @ModelAttribute @Valid CreateUserForm form,
            BindingResult result) {

        if (result.hasErrors()) {
            ModelAndView mav = new ModelAndView("users/form");
            mav.addObject("roles", User.Role.values());
            return mav;
        }

        UserResponse created = userService.create(form);
        ModelAndView mav = new ModelAndView("redirect:/users/" + created.id());
        return mav;
    }

    // ── Setting HTTP status code: ──────────────────────────────────────
    @GetMapping("/gone/{id}")
    public ModelAndView gone(@PathVariable Long id) {
        ModelAndView mav = new ModelAndView("users/gone");
        mav.setStatus(HttpStatus.GONE);        // 410 Gone
        mav.addObject("id", id);
        return mav;
    }

    // ── Returning null — signals no view should be rendered: ──────────
    // Spring MVC assumes the handler wrote directly to HttpServletResponse:
    @GetMapping("/raw")
    public ModelAndView rawResponse(HttpServletResponse response)
            throws IOException {
        response.getWriter().write("raw text output");
        return null;   // or new ModelAndView() with no view set
    }
}

// ── In @ExceptionHandler — ModelAndView is natural for error pages: ───
@ControllerAdvice   // note: @ControllerAdvice, not @RestControllerAdvice
public class ErrorPageHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ModelAndView handleNotFound(UserNotFoundException ex) {
        ModelAndView mav = new ModelAndView("error/404");
        mav.addObject("message", ex.getMessage());
        mav.setStatus(HttpStatus.NOT_FOUND);
        return mav;
    }

    @ExceptionHandler(Exception.class)
    public ModelAndView handleAll(Exception ex) {
        ModelAndView mav = new ModelAndView("error/500");
        mav.addObject("message", "An unexpected error occurred");
        mav.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
        return mav;
    }
}

ModelAndView vs String + Model

Both approaches produce the same result, so the choice is about readability. For a method that always renders one template, String plus Model is shorter and is what most Spring MVC code uses. Reach for ModelAndView when the method builds its view name from logic, when it must set an HTTP status, or when you write an error handler that renders an HTML page. A practical rule: start with String and Model, and switch to ModelAndView only when a method needs the extra control. Mixing both styles in one controller is fine, but keep each method consistent with itself. Whichever you choose, there is no performance difference, because both are converted to the same view name and model before the ViewResolver runs.
Java
// ── Use String + Model when: ─────────────────────────────────────────
// - The view name is always the same for the method
// - You prefer parameter injection over object construction
// - The method is straightforward with no branching

@GetMapping
public String list(Model model) {
    model.addAttribute("users", userService.findAll());
    return "users/list";
}

// ── Use ModelAndView when: ────────────────────────────────────────────
// - The view name is determined dynamically (branching logic)
// - You need to set the HTTP status code
// - You are writing a @ControllerAdvice error handler
// - You prefer a single return value over separate model and view name
// - Legacy code or team conventions require it

@GetMapping("/{id}")
public ModelAndView detail(@PathVariable Long id,
                            @RequestParam boolean print) {
    ModelAndView mav = new ModelAndView(print ? "users/print" : "users/detail");
    mav.addObject("user", userService.findById(id));
    if (print) mav.addObject("printDate", LocalDate.now());
    return mav;
}

// ── They are fully interchangeable — no performance difference: ───────
// Spring MVC converts both into the same internal model + view name pair
// before passing to the ViewResolver.

Testing ModelAndView Controllers

You can verify the view name, model contents and status of a controller without starting a server by using MockMvc in a @WebMvcTest test. The view() and model() matchers read directly from the ModelAndView the controller returned, so the same assertions work whether the method returns a String or a ModelAndView.
Java
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;

@Test
void detailPageRendersUserView() throws Exception {
    mockMvc.perform(get("/users/1/mav"))
        .andExpect(status().isOk())
        .andExpect(view().name("users/detail"))
        .andExpect(model().attributeExists("user"));
}

Common Mistakes with ModelAndView

- Forgetting to set a view name. If a ModelAndView has no view, Spring derives a default view name from the request URL, which is rarely what you intend. Always pass a view name to the constructor or call setViewName(). - Mixing view names with @RestController. A @RestController writes a returned String straight to the response body, so "users/detail" would appear as plain text instead of rendering a template. Use @Controller for HTML pages. - Relying on addObject(value) without a name. Spring generates the attribute name from the value's type, and for a List of User objects that name is userList, which may not match the name your template uses. Pass an explicit name. - Putting sensitive values on a redirect ModelAndView. Simple attributes may be appended to the redirect URL as query parameters. - Returning null by accident. It tells Spring the response was already written, so the browser can receive an empty page.