Close

Spring MVC - Binding Java Backing Objects with Custom Converters

[Last Updated: Mar 18, 2026]

Instead of using PropertyEditor, we can use Spring conversion service for strings to type conversion. Please check out our Spring core Conversion Service tutorial to get familiar with conversion service.

Spring MVC uses a single instance of ConversionService interface per container. This interface is the main interface Of the conversion service API.

In this example we are going to register a custom converter to map URI template variable to a Java object. This is an alternative to Spring implicit mapping of the path variable and request parameters to object properties, but with additional benefit; that is we can retrieve binding object from data layer or we can enrich the object on the fly.

We are going to create a custom converter to convert tradeId to Trade object. The Trade object is retrieved from data service based on trade id extracted from URI template.



Creating the backing object


public class Trade {

    private long tradeId;
    private String buySell;
    private String buyCurrency;
    private String sellCurrency;

    //getters and setters
}



Creating the Converter


import org.springframework.core.convert.converter.Converter;

public class TradeIdToTradeConverter implements Converter<String, Trade> {

    private TradeService tradeService;

    public TradeIdToTradeConverter (TradeService tradeService) {
        this.tradeService = tradeService;
    }

    @Override
    public Trade convert (String id) {
        try {
            Long tradeId = Long.valueOf(id);
            return tradeService.getTradeById(tradeId);
        } catch (NumberFormatException e) {
            return null;
        }
    }
}

Registering the Converter


package com.logicbig.example;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@EnableWebMvc
@Configuration
@Import(MyViewConfig.class)
public class TradeConfig extends WebMvcConfigurerAdapter {

    @Bean
    public TradeController tradeParamRequestController() {
        return new TradeController();
    }

    @Bean
    public TradeService tradeService(){
        return new TradeService();
    }

    @Override
    public void addFormatters (FormatterRegistry registry) {
        registry.addConverter(new TradeIdToTradeConverter(tradeService()));
    }
}


What is WebMvcConfigurerAdapter?

WebMvcConfigurerAdapter implements WebMvcConfigurer which defines callback methods to customize the Java-based configuration for Spring MVC enabled via @EnableWebMvc.

In above example, we are overriding the method addFormatters(FormatterRegistry registry) to add our custom converter. Generally this method is used to add Converters and Formatters in addition to the ones registered by default. Check out this tutorial too.



Creating the Controller


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

    @RequestMapping("/{trade}")
    public String handleTradeRequest (Trade trade, Model model) {

        if (trade.getTradeId() == 0) {
            model.addAttribute("msg", "No trade found");
            return "no-trade-page";
        }
        return "trade-page";
    }
}

Important: The name of the template variable has to be same as backing object parameter name of the handler method, otherwise Spring will map the template variable to object properties directly and our custom converter won't be used.



trade-page.jsp


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

Note that, we are using trade object reference in our jsp page, that's because the Trade object is implicitly populated in Model object by Spring; that happens even before our controller's handler method gets called.



Running Example

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

mvn jetty:run

Output in Browser:

http://localhost:8080/spring-custom-converter/trades/9

Unit Test

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 = TradeConfig.class)
@WebAppConfiguration
public class TradeControllerTest {

    private final MockMvcTester mockMvcTester;

    @Autowired
    public TradeControllerTest(WebApplicationContext wac) {
        // Initialize the tester directly from the WebApplicationContext
        this.mockMvcTester = MockMvcTester.from(wac);
    }

    @Test
    void testUserController() {
        // Execute the GET request and assert the status using AssertJ fluent style
        assertThat(
                mockMvcTester.get()
                             .uri("/trades/{id}", 2))
                .hasStatusOk();
    }
}
mvn clean test -Dtest=TradeControllerTest

Output

$ mvn clean test -Dtest=TradeControllerTest
[INFO] Scanning for projects...
[INFO]
[INFO] ------------< com.logicbig.example:spring-custom-converter >------------
[INFO] Building spring-custom-converter 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ war ]---------------------------------
[INFO]
[INFO] --- clean:3.2.0:clean (default-clean) @ spring-custom-converter ---
[INFO] Deleting D:\example-projects\spring-mvc\spring-custom-converter\target
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ spring-custom-converter ---
[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-custom-converter\src\main\resources
[INFO]
[INFO] --- compiler:3.15.0:compile (default-compile) @ spring-custom-converter ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 6 source files with javac [debug target 25] to target\classes
[INFO] /D:/LogicBig/example-projects/spring-mvc/spring-custom-converter/src/main/java/com/logicbig/example/TradeService.java: D:\example-projects\spring-mvc\spring-custom-converter\src\main\java\com\logicbig\example\TradeService.java uses unchecked or unsafe operations.
[INFO] /D:/LogicBig/example-projects/spring-mvc/spring-custom-converter/src/main/java/com/logicbig/example/TradeService.java: Recompile with -Xlint:unchecked for details.
[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ spring-custom-converter ---
[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-custom-converter\src\test\resources
[INFO]
[INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ spring-custom-converter ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 2 source files with javac [debug target 25] to target\test-classes
[INFO]
[INFO] --- surefire:3.2.5:test (default-test) @ spring-custom-converter ---
[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
Trade{tradeId=2, buySell='Buy', buyCurrency='NIO', sellCurrency='PHP'}
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.049 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: 6.809 s
[INFO] Finished at: 2026-03-18T12:34:56+08:00
[INFO] ------------------------------------------------------------------------
INFO: Completed initialization in 2 ms

Example Project

Dependencies and Technologies Used:

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

    Version compatibilities of spring-webmvc with this example:

      javax.servlet-api:3.x
    • 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 - Using Custom Converters for request mapping Select All Download
  • spring-custom-converter
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • TradeController.java
          • webapp
            • WEB-INF
              • views
        • test
          • java
            • com
              • logicbig
                • example

    See Also

    Join