Data Binding Techniques for Spring MVC Controllers
Controller methods in Spring MVC can accept a variety of parameter types and have request parameters automatically bound to them. The framework converts form fields, query parameters, and path variables into method arguments using simple naming conventions and optional annotations.
Primitive Type Binding
A primitive type is bound when the request parameter name matches the method parameter name. If no matching parameter is present or the value is empty, a conversion exception will ocurr for types like int. Therefore, use primitives onlly when a value is guaranteed.
@RequestMapping("/order")
public void createOrder(int quantity) {
// ...
}
HTML form (POST):
<form action="/order" method="post">
<input name="quantity" value="5" type="text"/>
<input type="submit" value="Place Order"/>
</form>
The @RequestParam annotation allows the parameter name to differ from the method argument, or makes a parameter optional with a default value.
@RequestMapping("/order")
public void createOrder(@RequestParam("qty") int quantity) {
// binds "qty" request parameter to quantity
}
Wrapper Type Binding
Using wrapper classes, such as Integer, Double, or Boolean, avoids conversion errors when the incoming value is null or an empty string. A missing parameter results in a null argument.
@RequestMapping("/process")
public void process(@RequestParam(required = false) Integer count) {
// count is null if not provided
}
The same principle applies without @RequestParam if the parameter name directly matches the form field name.
Custom Object Binding
Spring automatically maps form fields to the properties of a custom JavaBean. The field names in the request must correspond to the property names of the object.
Define a model class:
public class Product {
private String name;
private Double price;
// getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
}
Controller signature:
@RequestMapping("/saveProduct")
public void save(Product product) {
// populated automatically
}
Form markup:
<form action="/saveProduct" method="post">
<input name="name" value="Laptop" type="text"/>
<input name="price" value="999.99" type="text"/>
<input type="submit"/>
</form>
Nested Object Graphs
When a bean contains other beans as properties, dot notation in the input name is used to traverse the object graph.
Model:
public class Address {
private String city;
private String zipCode;
// getters & setters
}
public class Customer {
private String fullName;
private Address billingAddress;
// getters & setters
}
Controller:
@RequestMapping("/register")
public void register(Customer customer) {
System.out.println(customer.getBillingAddress().getCity());
}
Form:
<form action="/register" method="post">
<input name="fullName" value="Jane Doe"/>
<input name="billingAddress.city" value="San Francisco"/>
<input name="billingAddress.zipCode" value="94105"/>
<input type="submit"/>
</form>
Binding Collections of Objects
Collections such as List, Set, and Map must be wrapped inside a form-backing object. They cannot appear directly as top-level method parameters.
Binding a List
Create a wrapper class:
public class Item {
private String sku;
private int quantity;
// getters and setters
}
public class ShoppingCart {
private List<Item> items;
// getter and setter
}
Form inputs use indexed bracket notation:
<form action="/checkout" method="post">
<input name="items[0].sku" value="BOOK123"/>
<input name="items[0].quantity" value="2"/>
<input name="items[1].sku" value="PEN789"/>
<input name="items[1].quantity" value="5"/>
<input type="submit"/>
</form>
Controller:
@RequestMapping("/checkout")
public void checkout(ShoppingCart cart) {
for (Item item : cart.getItems()) {
System.out.println(item.getSku() + " x " + item.getQuantity());
}
}
Spring creates a list whose size equals the maximum index encountered plus one. Items at skipped indices will be present as null. For example, if indices 0, 1, and 20 are submitted, the list will contain 21 elements; indices 2–19 will be null.
Binding a Set
A Set requires initialised placeholder elements inside the wrapper’s constructor, because Spring cannot arbitrarily grow a set. The number of placeholders should match or exceed the maximum index used in the form.
Wrapper:
public class Attendee {
private String name;
private String email;
// getters & setters
}
public class RegistrationSetForm {
private Set<Attendee> attendees = new HashSet<>();
public RegistrationSetForm() {
attendees.add(new Attendee());
attendees.add(new Attendee());
attendees.add(new Attendee());
}
// getter & setter
}
The controller method accepts RegistrationSetForm, and the form uses the same indexed naming as with lists. If the maximum index exceeds the initial set size, an InvalidPropertyException is thrown.
Binding a Map
A Map offers greater flexibility. The key is specified in the input name using single quotes.
Wrapper:
public class ConfigurationForm {
private Map<String, String> settings;
// getter & setter
}
Form:
<form action="/saveConfig" method="post">
<input name="settings['timeout']" value="30"/>
<input name="settings['retries']" value="3"/>
<input type="submit"/>
</form>
Controller:
@RequestMapping("/saveConfig")
public void saveConfig(ConfigurationForm form) {
for (Map.Entry<String, String> entry : form.getSettings().entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
}
Spring populates the map with the exact keys supplied in the request. No placeholder entries are necessary. This approach is widely used for dynamic key-value pairs.