In this tutorial, we will learn how to create and register a custom HttpMessageConverter. The converter will convert the request csv body data to user defined object. The converter will also convert the response user object to csv data. We are going to use OpenCSV parser library for Java object to CSV (and vice-versa) conversion.
Example
Creating the Controller
package com.logicbig.example;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Controller
public class ExampleController {
private List<Employee> employeeSavedList = new ArrayList<>();
@PostMapping(
value = "/newEmployee",
consumes = "text/csv",
produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseBody
@ResponseStatus(HttpStatus.OK)
public String handleRequest(@RequestBody EmployeeList employeeList) {
employeeSavedList.addAll(employeeList.getList());
String s = String.format("Employees saved, size: %s",
employeeList.getList().size());
return s;
}
@GetMapping(
value = "/employeeList",
produces = "text/csv")
@ResponseBody
@ResponseStatus(HttpStatus.OK)
public EmployeeList getEmployees() {
EmployeeList employeeList = new EmployeeList();
employeeList.setList(employeeSavedList);
return employeeList;
}
}
package com.logicbig.example;
public class EmployeeList extends ListParam<Employee> {
}
package com.logicbig.example;
import java.util.List;
public class ListParam<T> {
private List<T> list;
public List<T> getList () {
return list;
}
public void setList (List<T> list) {
this.list = list;
}
}
package com.logicbig.example;
import com.opencsv.bean.CsvBindByName;
public class Employee {
@CsvBindByName(column = "ID")
private String id;
@CsvBindByName(column = "NAME")
private String name;
@CsvBindByName(column = "PHONE_NUMBER")
private String phoneNumber;
.............
}
Creating the Converter
package com.logicbig.example;
import com.opencsv.CSVReader;
import com.opencsv.CSVWriter;
import com.opencsv.bean.CsvToBean;
import com.opencsv.bean.HeaderColumnNameMappingStrategy;
import com.opencsv.bean.StatefulBeanToCsv;
import com.opencsv.bean.StatefulBeanToCsvBuilder;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
public class CsvHttpMessageConverter<T, L extends ListParam<T>>
extends AbstractHttpMessageConverter<L> {
public CsvHttpMessageConverter () {
super(new MediaType("text", "csv"));
}
@Override
protected boolean supports (Class<?> clazz) {
return ListParam.class.isAssignableFrom(clazz);
}
@Override
protected L readInternal (Class<? extends L> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
HeaderColumnNameMappingStrategy<T> strategy =
new HeaderColumnNameMappingStrategy<>();
Class<T> t = toBeanType(clazz.getGenericSuperclass());
strategy.setType(t);
CSVReader csv = new CSVReader(new InputStreamReader(inputMessage.getBody()));
CsvToBean<T> csvToBean = new CsvToBean<>();
csvToBean.setMappingStrategy(strategy);
csvToBean.setCsvReader(csv);
List<T> beanList = csvToBean.parse();
try {
L l = clazz.newInstance();
l.setList(beanList);
return l;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
@Override
protected void writeInternal (L l, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
HeaderColumnNameMappingStrategy<T> strategy =
new HeaderColumnNameMappingStrategy<>();
strategy.setType(toBeanType(l.getClass().getGenericSuperclass()));
OutputStreamWriter outputStream = new OutputStreamWriter(outputMessage.getBody());
StatefulBeanToCsv<T> beanToCsv =
new StatefulBeanToCsvBuilder(outputStream)
.withQuotechar(CSVWriter.NO_QUOTE_CHARACTER)
.withMappingStrategy(strategy)
.build();
try {
beanToCsv.write(l.getList());
outputStream.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("unchecked")
private Class<T> toBeanType (Type type) {
return (Class<T>) ((ParameterizedType) type).getActualTypeArguments()[0];
}
}
Registering the Converter
package com.logicbig.example;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverters;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@EnableWebMvc
@ComponentScan("com.logicbig.example")
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(
HttpMessageConverters.ServerBuilder builder) {
builder.addCustomConverter(new CsvHttpMessageConverter<>());
}
}
Integration Test
package com.logicbig.example;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
@SpringJUnitWebConfig(AppConfig.class)
public class ControllerTest {
@Autowired
private WebApplicationContext wac;
private MockMvcTester mockMvcTester;
@BeforeEach
public void setup() {
this.mockMvcTester = MockMvcTester.from(this.wac);
}
@Test
public void testConsumerController() {
String employeesAsCsv = getNewEmployeeListInCsv();
assertThat(this.mockMvcTester.post().uri("/newEmployee")
.contentType("text/csv")
.accept(MediaType.TEXT_PLAIN_VALUE)
.content(employeesAsCsv)
.exchange())
.hasStatusOk()
.bodyText().isEqualTo("Employees saved, size: 3");
assertThat(this.mockMvcTester.get().uri("/employeeList")
.accept("text/csv")
.exchange())
.hasStatusOk()
.bodyText().isEqualTo(employeesAsCsv);
}
public String getNewEmployeeListInCsv() {
return "ID,NAME,PHONE_NUMBER\n1,Joe,123-212-3233\n2,Sara,132-232-3111\n" +
"3,Mike,111-222-3333\n";
}
}
mvn clean test -Dtest="ControllerTest" Output$ mvn clean test -Dtest="ControllerTest" [INFO] Scanning for projects... [WARNING] [WARNING] Some problems were encountered while building the effective model for com.logicbig.example:custom-message-converter:war:1.0-SNAPSHOT [WARNING] 'build.plugins.plugin.version' for org.apache.maven.plugins:maven-war-plugin is missing. @ line 50, column 21 [WARNING] [WARNING] It is highly recommended to fix these problems because they threaten the stability of your build. [WARNING] [WARNING] For this reason, future Maven versions might no longer support building such malformed projects. [WARNING] [INFO] [INFO] -----------< com.logicbig.example:custom-message-converter >------------ [INFO] Building custom-message-converter 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ war ]--------------------------------- [INFO] [INFO] --- clean:3.2.0:clean (default-clean) @ custom-message-converter --- [INFO] Deleting D:\example-projects\spring-mvc\custom-message-converter\target [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ custom-message-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\custom-message-converter\src\main\resources [INFO] [INFO] --- compiler:3.3:compile (default-compile) @ custom-message-converter --- [INFO] Changes detected - recompiling the module! [INFO] Compiling 6 source files to D:\example-projects\spring-mvc\custom-message-converter\target\classes [INFO] /D:/LogicBig/example-projects/spring-mvc/custom-message-converter/src/main/java/com/logicbig/example/CsvHttpMessageConverter.java: D:\example-projects\spring-mvc\custom-message-converter\src\main\java\com\logicbig\example\CsvHttpMessageConverter.java uses or overrides a deprecated API. [INFO] /D:/LogicBig/example-projects/spring-mvc/custom-message-converter/src/main/java/com/logicbig/example/CsvHttpMessageConverter.java: Recompile with -Xlint:deprecation for details. [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ custom-message-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\custom-message-converter\src\test\resources [INFO] [INFO] --- compiler:3.3:testCompile (default-testCompile) @ custom-message-converter --- [INFO] Changes detected - recompiling the module! [INFO] Compiling 2 source files to D:\example-projects\spring-mvc\custom-message-converter\target\test-classes [INFO] [INFO] --- surefire:3.2.5:test (default-test) @ custom-message-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.ControllerTest [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.864 s -- in com.logicbig.example.ControllerTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 11.518 s [INFO] Finished at: 2026-07-25T11:48:38+08:00 [INFO] ------------------------------------------------------------------------ INFO: Completed initialization in 3 ms
Example ProjectDependencies and Technologies Used: - spring-webmvc 7.0.6 (Spring Web MVC)
Version Compatibility: 4.1.3.RELEASE - 7.0.6 Version compatibilities of spring-webmvc with this example: 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)
- opencsv 5.12.0 (A simple library for reading and writing CSV in Java)
- hamcrest 3.0 (Core API and libraries of hamcrest matcher framework)
- assertj-core 3.26.3 (Rich and fluent assertions for testing in Java)
- JDK 25
- Maven 3.9.11
|