Close

Spring MVC - Binding Request Parameters and Path Variables to Java Backing Objects

[Last Updated: Mar 18, 2026]

The default Spring Data binding mechanism allows us to bind the HTTP request details to the application specific objects.

The object must obey JavaBean convention.

Spring uses WebDataBinder which is a subclass of DataBinder.

WebDataBinder uses PropertyEditors/ConversionService to bind request parameter to handler method arguments. The PropertyEditor concept is part of the JavaBeans specification.

JavaBeans specs and API quick tutorial

Also see Spring Core PropertyEditors support and Conversion Services.



Examples

To try examples, run embedded Jetty (configured in pom.xml of example project below):

mvn jetty:run

Binding Request Parameters

Using @RequestParam

As we saw in the previous tutorials that one way to bind request parameters is to use @RequestParam annotation. In this example we are mapping /trades?buySell=buy&buyCurrency=EUR&sellCurrency=USD:

@Controller
@RequestMapping("trades")
public class TradeController {

    @RequestMapping
    public String handleTradeRequest (@RequestParam("buySell") String buySell,
                                      @RequestParam("buyCurrency") String buyCurrency,
                                      @RequestParam("sellCurrency") String sellCurrency,
                                      Model map) {
        String msg = String.format(
                        "trade request. buySell: %s, buyCurrency: %s, sellCurrency: %s",
                         buySell, buyCurrency, sellCurrency);
        map.addAttribute("msg", msg);
        return "my-page";
    }
}


Binding Java Object

@RequestParam should be used if there are few parameters to bind with. But as number of request parameters increases our controller method becomes unmaintainable. There, we should create a Java class to map the request parameters to the class properties. The class should be standard JavaBean and property name should match the request param names.

Creating Object to capture request params:

public class Trade {

    private String buySell;
    private String buyCurrency;
    private String sellCurrency;

    public String getBuySell () {
        return buySell;
    }

    public void setBuySell (String buySell) {
        this.buySell = buySell;
    }

   .................
}

Using above object in the controller:

@Controller
@RequestMapping("trades")
public class TradeController {
    @RequestMapping
    public String handleTradeRequest (Trade trade,
                                      Model map) {
        String msg = String.format(
                          "trade request. buySell: %s, buyCurrency: %s, sellCurrency: %s",
                           trade.getBuySell(), trade.getBuyCurrency(),
                           trade.getSellCurrency());
        map.addAttribute("msg", msg);
        return "my-page";
    }

Note we don't have to use the annotation @RequestParam with binding object.



We can also use our binding object in the JSP view without explicitly adding that into the Model object.

<%@ page language="java"
    contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<html>
<body>
   <h3> Message : ${msg} <h3>
   <p>BuySell: ${trade.buySell}</p>
   <p>Buy Currency: ${trade.buyCurrency}</p>
   <p>Sell Currency: ${trade.sellCurrency}</p>
</body>
</html>

In fact the Model object is already populated with our binding object before the handler method is called by Spring. You can see what's inside Model by printing it in the handler.




Binding Path Variables

Using @PathVariables

Modifying our above example to use URI template variables:

@Controller
@RequestMapping("trades")
public class TradeController {

    @RequestMapping("{buySell}/{buyCurrency}/{sellCurrency}")
    public String handleTradeRequest3 (@PathVariable("buySell") String buySell,
                                       @PathVariable("buyCurrency") String buyCurrency,
                                       @PathVariable("sellCurrency") String sellCurrency,
                                       Model map) {
        String msg = String.format(
                       "trade request. buySell: %s, buyCurrency: %s, sellCurrency: %s",
                       buySell, buyCurrency, sellCurrency);
        map.addAttribute("msg", msg);
        return "my-page";
    }
}

The above handler will map to the request: /trades/buy/EUR/USD



Using Java Object

We don't have to use @PathVariable annotation:

@Controller
@RequestMapping("trades")
public class TradeController {

    @RequestMapping("{buySell}/{buyCurrency}/{sellCurrency}")
    public String handleTradeRequest (Trade trade, Model map) {
        String msg = String.format(
                           "trade request. buySell: %s, buyCurrency: %s, sellCurrency: %s",
                           trade.getBuySell(), trade.getBuyCurrency(),
                           trade.getSellCurrency());

        map.addAttribute("msg", msg);
        return "my-page";
    }
}

Unit Tests

package com.logicbig.example;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;

@SpringJUnitConfig(classes = MyWebConfig.class)
@WebAppConfiguration
public class TradeControllerTest {

    private final MockMvcTester mockMvcTester;

    @Autowired
    public TradeControllerTest(WebApplicationContext wac) {
        this.mockMvcTester = MockMvcTester.from(wac);
    }

    @Test
    void testUserController() {
        // 1. Using explicit param() method (Cleaner than manual string building)
        assertThat(mockMvcTester.get().uri("/trades/paramTest")
                                .param("buySell", "buy")
                                .param("buyCurrency", "EUR")
                                .param("sellCurrency", "USD"))
                .hasStatusOk();

        // 2. Query Params via URI (Still supported, but param() is usually preferred)
        assertThat(mockMvcTester.get()
                                .uri("/trades?buySell=buy&"
                                             + "buyCurrency=EUR&"
                                             + "sellCurrency=USD"))
                .hasStatusOk();

        // 3. Path Variables (Simple template)
        assertThat(mockMvcTester.get()
                                .uri("/trades/buy/EUR/USD"))
                .hasStatusOk();

        // 4. Path Variables using URI template arguments (Best Practice)
        assertThat(mockMvcTester.get()
                                .uri("/trades/pathTest/{action}/{buy}/{sell}",
                                     "buy", "EUR", "USD"))
                .hasStatusOk();
    }
}
mvn test -Dtest="TradeControllerTest"

Output

$ mvn test -Dtest="TradeControllerTest"
[INFO] Scanning for projects...
[INFO]
[INFO] ------------< com.logicbig.example:spring-mvc-data-binding >------------
[INFO] Building spring-mvc-data-binding 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ war ]---------------------------------
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ spring-mvc-data-binding ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\spring-mvc-data-binding\src\main\resources
[INFO]
[INFO] --- compiler:3.15.0:compile (default-compile) @ spring-mvc-data-binding ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ spring-mvc-data-binding ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\spring-mvc-data-binding\src\test\resources
[INFO]
[INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ spring-mvc-data-binding ---
[INFO] Nothing to compile - all classes are up to date.
[INFO]
[INFO] --- surefire:3.2.5:test (default-test) @ spring-mvc-data-binding ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[WARNING] file.encoding cannot be set as system property, use <argLine>-Dfile.encoding=...</argLine> instead
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.logicbig.example.TradeControllerTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.477 s -- in com.logicbig.example.TradeControllerTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.449 s
[INFO] Finished at: 2026-03-18T11:54:07+08:00
[INFO] ------------------------------------------------------------------------
INFO: Completed initialization in 1 ms
Mar 18, 2026 11:54:07 AM com.logicbig.example.TradeController handleTradeRequest
INFO: trade request. buySell: buy, buyCurrency: EUR, sellCurrency: USD
Mar 18, 2026 11:54:07 AM com.logicbig.example.TradeController handleTradeRequest
INFO: trade request. buySell: buy, buyCurrency: EUR, sellCurrency: USD
Mar 18, 2026 11:54:07 AM com.logicbig.example.TradeController handleTradeRequest2
INFO: trade request. buySell: buy, buyCurrency: EUR, sellCurrency: USD
Mar 18, 2026 11:54:07 AM com.logicbig.example.TradeController handleTradeRequest3
INFO: trade request. buySell: buy, buyCurrency: EUR, sellCurrency: USD

Example Project

Dependencies and Technologies Used:

  • spring-webmvc 7.0.6 (Spring Web MVC)
     Version Compatibility: 3.2.3.RELEASE - 7.0.6Version List
    ×

    Version compatibilities of spring-webmvc with this example:

      javax.servlet-api:3.x
    • 3.2.3.RELEASE
    • 3.2.4.RELEASE
    • 3.2.5.RELEASE
    • 3.2.6.RELEASE
    • 3.2.7.RELEASE
    • 3.2.8.RELEASE
    • 3.2.9.RELEASE
    • 3.2.10.RELEASE
    • 3.2.11.RELEASE
    • 3.2.12.RELEASE
    • 3.2.13.RELEASE
    • 3.2.14.RELEASE
    • 3.2.15.RELEASE
    • 3.2.16.RELEASE
    • 3.2.17.RELEASE
    • 3.2.18.RELEASE
    • 4.0.0.RELEASE
    • 4.0.1.RELEASE
    • 4.0.2.RELEASE
    • 4.0.3.RELEASE
    • 4.0.4.RELEASE
    • 4.0.5.RELEASE
    • 4.0.6.RELEASE
    • 4.0.7.RELEASE
    • 4.0.8.RELEASE
    • 4.0.9.RELEASE
    • 4.1.0.RELEASE
    • 4.1.1.RELEASE
    • 4.1.2.RELEASE
    • 4.1.3.RELEASE
    • 4.1.4.RELEASE
    • 4.1.5.RELEASE
    • 4.1.6.RELEASE
    • 4.1.7.RELEASE
    • 4.1.8.RELEASE
    • 4.1.9.RELEASE
    • 4.2.0.RELEASE
    • 4.2.1.RELEASE
    • 4.2.2.RELEASE
    • 4.2.3.RELEASE
    • 4.2.4.RELEASE
    • 4.2.5.RELEASE
    • 4.2.6.RELEASE
    • 4.2.7.RELEASE
    • 4.2.8.RELEASE
    • 4.2.9.RELEASE
    • 4.3.0.RELEASE
    • 4.3.1.RELEASE
    • 4.3.2.RELEASE
    • 4.3.3.RELEASE
    • 4.3.4.RELEASE
    • 4.3.5.RELEASE
    • 4.3.6.RELEASE
    • 4.3.7.RELEASE
    • 4.3.8.RELEASE
    • 4.3.9.RELEASE
    • 4.3.10.RELEASE
    • 4.3.11.RELEASE
    • 4.3.12.RELEASE
    • 4.3.13.RELEASE
    • 4.3.14.RELEASE
    • 4.3.15.RELEASE
    • 4.3.16.RELEASE
    • 4.3.17.RELEASE
    • 4.3.18.RELEASE
    • 4.3.19.RELEASE
    • 4.3.20.RELEASE
    • 4.3.21.RELEASE
    • 4.3.22.RELEASE
    • 4.3.23.RELEASE
    • 4.3.24.RELEASE
    • 4.3.25.RELEASE
    • 4.3.26.RELEASE
    • 4.3.27.RELEASE
    • 4.3.28.RELEASE
    • 4.3.29.RELEASE
    • 4.3.30.RELEASE
    • 5.0.0.RELEASE
    • 5.0.1.RELEASE
    • 5.0.2.RELEASE
    • 5.0.3.RELEASE
    • 5.0.4.RELEASE
    • 5.0.5.RELEASE
    • 5.0.6.RELEASE
    • 5.0.7.RELEASE
    • 5.0.8.RELEASE
    • 5.0.9.RELEASE
    • 5.0.10.RELEASE
    • 5.0.11.RELEASE
    • 5.0.12.RELEASE
    • 5.0.13.RELEASE
    • 5.0.14.RELEASE
    • 5.0.15.RELEASE
    • 5.0.16.RELEASE
    • 5.0.17.RELEASE
    • 5.0.18.RELEASE
    • 5.0.19.RELEASE
    • 5.0.20.RELEASE
    • 5.1.0.RELEASE
    • 5.1.1.RELEASE
    • 5.1.2.RELEASE
    • 5.1.3.RELEASE
    • 5.1.4.RELEASE
    • 5.1.5.RELEASE
    • 5.1.6.RELEASE
    • 5.1.7.RELEASE
    • 5.1.8.RELEASE
    • 5.1.9.RELEASE
    • 5.1.10.RELEASE
    • 5.1.11.RELEASE
    • 5.1.12.RELEASE
    • 5.1.13.RELEASE
    • 5.1.14.RELEASE
    • 5.1.15.RELEASE
    • 5.1.16.RELEASE
    • 5.1.17.RELEASE
    • 5.1.18.RELEASE
    • 5.1.19.RELEASE
    • 5.1.20.RELEASE
    • 5.2.0.RELEASE
    • 5.2.1.RELEASE
    • 5.2.2.RELEASE
    • 5.2.3.RELEASE
    • 5.2.4.RELEASE
    • 5.2.5.RELEASE
    • 5.2.6.RELEASE
    • 5.2.7.RELEASE
    • 5.2.8.RELEASE
    • 5.2.9.RELEASE
    • 5.2.10.RELEASE
    • 5.2.11.RELEASE
    • 5.2.12.RELEASE
    • 5.2.13.RELEASE
    • 5.2.14.RELEASE
    • 5.2.15.RELEASE
    • 5.2.16.RELEASE
    • 5.2.17.RELEASE
    • 5.2.18.RELEASE
    • 5.2.19.RELEASE
    • 5.2.20.RELEASE
    • 5.2.21.RELEASE
    • 5.2.22.RELEASE
    • 5.2.23.RELEASE
    • 5.2.24.RELEASE
    • 5.2.25.RELEASE
    • 5.3.0
    • 5.3.1
    • 5.3.2
    • 5.3.3
    • 5.3.4
    • javax.servlet-api:4.x
    • 5.3.5
    • 5.3.6
    • 5.3.7
    • 5.3.8
    • 5.3.9
    • 5.3.10
    • 5.3.11
    • 5.3.12
    • 5.3.13
    • 5.3.14
    • 5.3.15
    • 5.3.16
    • 5.3.17
    • 5.3.18
    • 5.3.19
    • 5.3.20
    • 5.3.21
    • 5.3.22
    • 5.3.23
    • 5.3.24
    • 5.3.25
    • 5.3.26
    • 5.3.27
    • 5.3.28
    • 5.3.29
    • 5.3.30
    • 5.3.31
    • 5.3.32
    • 5.3.33
    • 5.3.34
    • 5.3.35
    • 5.3.36
    • 5.3.37
    • 5.3.38
    • 5.3.39
    • javax.* -> jakarta.*
      jakarta.servlet-api:6.x
      Java 17 min
    • 6.0.0
    • 6.0.1
    • 6.0.2
    • 6.0.3
    • 6.0.4
    • 6.0.5
    • 6.0.6
    • 6.0.7
    • 6.0.8
    • 6.0.9
    • 6.0.10
    • 6.0.11
    • 6.0.12
    • 6.0.13
    • 6.0.14
    • 6.0.15
    • 6.0.16
    • 6.0.17
    • 6.0.18
    • 6.0.19
    • 6.0.20
    • 6.0.21
    • 6.0.22
    • 6.0.23
    • 6.1.0
    • 6.1.1
    • 6.1.2
    • 6.1.3
    • 6.1.4
    • 6.1.5
    • 6.1.6
    • 6.1.7
    • 6.1.8
    • 6.1.9
    • 6.1.10
    • 6.1.11
    • 6.1.12
    • 6.1.13
    • 6.1.14
    • 6.1.15
    • 6.1.16
    • 6.1.17
    • 6.1.18
    • 6.1.19
    • 6.1.20
    • 6.1.21
    • 6.2.0
    • 6.2.1
    • 6.2.2
    • 6.2.3
    • 6.2.4
    • 6.2.5
    • 6.2.6
    • 6.2.7
    • 6.2.8
    • 6.2.9
    • 6.2.10
    • 6.2.11
    • 6.2.12
    • 6.2.13
    • 6.2.14
    • 6.2.15
    • 6.2.16
    • 6.2.17
    • 7.0.0
    • 7.0.1
    • 7.0.2
    • 7.0.3
    • 7.0.4
    • 7.0.5
    • 7.0.6

    Versions in green have been tested.

  • spring-test 7.0.6 (Spring TestContext Framework)
  • jakarta.servlet-api 6.1.0 (Jakarta Servlet API documentation)
  • junit-jupiter-engine 6.0.3 (Module "junit-jupiter-engine" of JUnit)
  • assertj-core 3.26.3 (Rich and fluent assertions for testing in Java)
  • JDK 25
  • Maven 3.9.11

Spring MVC - Implicit conversion to Java Backing Objects Select All Download
  • spring-mvc-data-binding
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • TradeController.java
          • webapp
            • WEB-INF
              • views
        • test
          • java
            • com
              • logicbig
                • example

    See Also

    Join