HTTP Message Converters with the Spring Framework

In modern web application development with the Spring Framework, efficiently handling HTTP requests and responses involves converting between Java objects and HTTP message formats (e.g., JSON, XML, form data). HTTP Message Converters are the core components that enable this conversion—they deserialize HTTP request bodies into Java objects and serialize Java objects into HTTP response bodies. This blog explores their role, built-in implementations, customization, best practices, and real-world examples.

Table of Contents#

  1. Introduction to HTTP Message Converters
  2. How Spring MVC Uses Message Converters
  3. Built-in HTTP Message Converters
  4. Configuring Message Converters
  5. Creating Custom HTTP Message Converters
  6. Content Negotiation and Message Converters
  7. Best Practices
  8. Example: Building a REST API with Custom Converters
  9. Troubleshooting Common Issues
  10. References

1. Introduction to HTTP Message Converters#

HTTP Message Converters bridge the gap between:

  • Deserialization: Converting an HTTP request body (e.g., JSON/XML) into a Java object (used with @RequestBody).
  • Serialization: Converting a Java object into an HTTP response body (used with @ResponseBody).

Key Use Cases:#

  • RESTful APIs: Convert JSON/XML request/response bodies to/from domain objects.
  • Form submissions: Convert application/x-www-form-urlencoded data to Java beans.
  • Binary data: Handle file uploads/downloads (e.g., byte[]).

2. How Spring MVC Uses Message Converters#

Spring’s request processing lifecycle involves:

  1. Request Handling: For a @PostMapping with @RequestBody, Spring selects a converter based on the request’s Content-Type (e.g., application/json) and the target Java type (e.g., User).
  2. Response Handling: For a @GetMapping with @ResponseBody, Spring selects a converter based on the response’s Accept header (e.g., application/json) and the return type (e.g., User).

Converter Selection Logic:#

Spring maintains a list of HttpMessageConverter instances. The order of converters matters—converters earlier in the list have higher priority. Spring auto-registers converters based on classpath dependencies (e.g., Jackson for JSON, JAXB for XML).

3. Built-in HTTP Message Converters#

Spring provides out-of-the-box converters for common media types:

3.1 MappingJackson2HttpMessageConverter (JSON)#

  • Purpose: Converts between JSON and Java objects (using Jackson).
  • Trigger: Activated when Content-Type is application/json (request) or Accept is application/json (response).
  • Dependency: com.fasterxml.jackson.core:jackson-databind (auto-configured in Spring Boot).

3.2 Jaxb2RootElementHttpMessageConverter (XML)#

  • Purpose: Converts between XML and Java objects (using JAXB).
  • Trigger: Activated for application/xml when JAXB is on the classpath.
  • Requirement: Java classes must be annotated with @XmlRootElement, @XmlElement, etc.

3.3 FormHttpMessageConverter (Form Data)#

  • Purpose: Handles application/x-www-form-urlencoded or multipart/form-data (file uploads).
  • Trigger: Activated for form submissions (e.g., HTML forms).

3.4 StringHttpMessageConverter (Plain Text)#

  • Purpose: Converts between String and plain text (text/plain).

3.5 ByteArrayHttpMessageConverter (Binary Data)#

  • Purpose: Handles binary data (e.g., file uploads/downloads as byte[]).

4. Configuring Message Converters#

4.1 Spring MVC (Java Configuration)#

Extend WebMvcConfigurer to customize converters:

@Configuration
public class WebConfig implements WebMvcConfigurer {
 
    // Override to replace/extend converters
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // Add a custom converter
        converters.add(new MyCustomConverter());
        // Disable default converters (optional)
    }
 
    // Override to modify existing converters (e.g., configure Jackson)
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        for (HttpMessageConverter<?> converter : converters) {
            if (converter instanceof MappingJackson2HttpMessageConverter) {
                MappingJackson2HttpMessageConverter jacksonConverter = 
                    (MappingJackson2HttpMessageConverter) converter;
                ObjectMapper mapper = jacksonConverter.getObjectMapper();
                // Customize ObjectMapper (e.g., add modules, disable features)
                mapper.registerModule(new JavaTimeModule()); // For Java 8+ Date/Time
            }
        }
    }
}

4.2 Spring Boot (Auto-Configuration)#

Spring Boot auto-configures converters based on dependencies. To customize:

  • Global Jackson Configuration: Provide a Jackson2ObjectMapperBuilder bean:
    @Bean
    public Jackson2ObjectMapperBuilder objectMapperBuilder() {
        return new Jackson2ObjectMapperBuilder()
            .indentOutput(true)
            .dateFormat(new SimpleDateFormat("yyyy-MM-dd"))
            .modules(new Jdk8Module(), new JavaTimeModule());
    }
  • Custom Converters: Use WebMvcConfigurer as in Spring MVC.

5. Creating Custom HTTP Message Converters#

For unique media types (e.g., CSV, Protobuf), implement HttpMessageConverter (or extend AbstractHttpMessageConverter for simplicity).

Example: Custom CSV Converter#

Let’s create a converter to handle CSV (comma-separated values) for a User domain class:

Step 1: Define the Domain Class#

public class User {
    private String name;
    private int age;
 
    // Getters + Setters
}

Step 2: Implement the Converter#

public class CsvHttpMessageConverter extends AbstractHttpMessageConverter<List<User>> {
 
    public CsvHttpMessageConverter() {
        super(MediaType.valueOf("text/csv")); // Support "text/csv"
    }
 
    @Override
    protected boolean supports(Class<?> clazz) {
        return List.class.isAssignableFrom(clazz);
    }
 
    @Override
    protected List<User> readInternal(Class<? extends List<User>> clazz, 
                                     HttpInputMessage inputMessage) throws IOException {
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(inputMessage.getBody()));
        List<User> users = new ArrayList<>();
        String line;
        boolean isHeader = true;
 
        while ((line = reader.readLine()) != null) {
            if (isHeader) {
                isHeader = false;
                continue; // Skip header row
            }
            String[] parts = line.split(",");
            User user = new User();
            user.setName(parts[0]);
            user.setAge(Integer.parseInt(parts[1]));
            users.add(user);
        }
        return users;
    }
 
    @Override
    protected void writeInternal(List<User> users, 
                                HttpOutputMessage outputMessage) throws IOException {
        BufferedWriter writer = new BufferedWriter(
            new OutputStreamWriter(outputMessage.getBody()));
        writer.write("name,age\n"); // Write header
 
        for (User user : users) {
            writer.write(user.getName() + "," + user.getAge() + "\n");
        }
        writer.flush();
    }
}

Step 3: Register the Converter#

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new CsvHttpMessageConverter());
    }
}

Step 4: Use in a Controller#

@RestController
public class UserController {
 
    @PostMapping(value = "/users", consumes = "text/csv")
    public ResponseEntity<Void> createUsers(@RequestBody List<User> users) {
        // Save users to database
        return ResponseEntity.created(URI.create("/users")).build();
    }
 
    @GetMapping(value = "/users", produces = "text/csv")
    public ResponseEntity<List<User>> getUsers() {
        List<User> users = // Fetch users from service
        return ResponseEntity.ok(users);
    }
}

6. Content Negotiation and Message Converters#

Content negotiation determines the response format based on the client’s Accept header (or URL parameters/extensions). Spring uses this to select the appropriate converter.

Example: Configure Content Negotiation#

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer
            .favorPathExtension(false)       // Disable .json/.xml in URLs
            .favorParameter(true)            // Enable "?format=csv"
            .parameterName("format")         // Query parameter name
            .ignoreAcceptHeader(false)       // Respect "Accept" header
            .defaultContentType(MediaType.APPLICATION_JSON)
            .mediaType("json", MediaType.APPLICATION_JSON)
            .mediaType("csv", MediaType.valueOf("text/csv"));
    }
}

Client Request Example:#

  • GET /users?format=csv → Returns CSV.
  • GET /users with Accept: text/csv → Returns CSV.

7. Best Practices#

7.1 Choose the Right Converter#

  • Prefer Built-in Converters: Use Spring’s built-in converters (e.g., Jackson for JSON) for standard formats—they are optimized and well-tested.
  • Custom Converters Only for Unique Needs: Build custom converters only for non-standard media types (e.g., CSV, Protobuf).

7.2 Performance#

  • Streaming for Large Payloads: For large datasets, use streaming APIs (e.g., Jackson’s JsonParser/JsonGenerator) to avoid loading the entire payload into memory.
  • Lazy Loading: For related entities (e.g., Hibernate lazy collections), use DTOs to control serialization.

7.3 Error Handling#

  • Meaningful Errors: In custom converters, catch parsing exceptions and throw HttpMessageNotReadableException with a clear message (e.g., “Invalid CSV format: missing age column”).
  • Log Errors: Enable debug logging for org.springframework.web to diagnose serialization/deserialization issues.

8. Example: Building a REST API with Custom Converters#

Step 1: Project Setup#

Create a Spring Boot project with spring-boot-starter-web dependency.

Step 2: Test the API#

Use curl to test the CSV endpoints:

# Create users (CSV request)
curl -X POST -H "Content-Type: text/csv" --data "name,age\nJohn,30\nJane,25" http://localhost:8080/users
 
# Fetch users (CSV response)
curl -H "Accept: text/csv" http://localhost:8080/users

9. Troubleshooting Common Issues#

9.1 406 Not Acceptable#

  • Cause: The client’s Accept header (e.g., Accept: application/xml) does not match any registered converter’s media type.
  • Fix: Ensure the converter for application/xml is registered (e.g., JAXB is on the classpath) or adjust the Accept header.

9.2 415 Unsupported Media Type#

  • Cause: The request’s Content-Type (e.g., application/csv) is not supported by any converter.
  • Fix: Register a converter for application/csv or adjust the Content-Type header.

9.3 Serialization/Deserialization Errors#

  • Cause: Missing getters/setters (for Jackson), invalid XML/JSON structure, or custom converter bugs.
  • Fix: Validate object structure, enable Jackson’s SerializationFeature.INDENT_OUTPUT for debugging, or fix custom converter logic.

10. References#

By mastering HTTP message converters, you can build flexible, efficient RESTful APIs that handle diverse data formats. Whether using built-in converters or crafting custom solutions, Spring’s message conversion ecosystem empowers seamless integration between Java objects and HTTP.