Spring Boot applications often require handling diverse data formats during request processing and response generation. By implementing custom converters and configuring content negotiation strategies, developers can insure seamless data exchange between clients and servers regardless of the preferred media type.
Implementing Custom Data Converters
When a client submits form data, Spring Boot utilizes built-in converters to bind request parameters to Java objects. While the framework provides over a hundred default converters, specific business requirements often necessitate custom conversion logic.
Defining the Conversion Logic
Consider a scenario where a vehicle description is submitted as a single string containing the model name and price, separated by a comma. To map this string to a Vehicle object, a custom converter must be registered.
Input Format Example:
<!-- Form input submitting a combined string -->
Vehicle Info: <input name="vehicleData" value="Storm Cruiser,85000.50"><br/>
Registering the Converter
Create a configuration class to register the custom converter with the FormatterRegistry. This involves implementing the WebMvcConfigurer interface.
package com.example.demo.config;
import com.example.demo.model.Vehicle;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.core.convert.converter.Converter;
import org.springframework.util.StringUtils;
@Configuration
public class MvcRegistryConfig {
@Bean
public WebMvcConfigurer mvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addFormatters(FormatterRegistry registry) {
// Register a converter that transforms String input into a Vehicle object
registry.addConverter(new Converter<String, Vehicle>() {
@Override
public Vehicle convert(String rawInput) {
if (!StringUtils.hasText(rawInput)) {
return null;
}
String[] parts = rawInput.split(",");
Vehicle model = new Vehicle();
// Assign parsed values to the object properties
model.setModelName(parts[0].trim());
model.setCost(Double.parseDouble(parts[1].trim()));
return model;
}
});
}
};
}
}
Converter Registration Details
The converters are stored internally within a ConcurrentHashMap. When debugging, you can observe the registered converters in the registry. If multiple converters are defined for the same source and target type pair, the last registered one will override previous definitions.
For instance, registering two converters for String to Vehicle will result in the second implementation replacing the first. This behavior allows for dynamic updates but requires careful management to avoid unintended overrides.
Handling JSON Responses
Spring Boot simplifies returning data in JSON format. When the web starter dependency is included, the necessary libraries (such as Jackson) are automatically configured.
Creating a Response Endpoint
Define a controller method that returns a Java object. By using the @ResponseBody annotation, the return value is serialized directly into the HTTP response body.
package com.example.demo.controller;
import com.example.demo.model.EntityRecord;
import com.example.demo.model.Vehicle;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Date;
@Controller
public class DataEndpoint {
@GetMapping(value = "/fetchRecord")
@ResponseBody
public EntityRecord fetchRecord() {
EntityRecord record = new EntityRecord();
record.setIdentifier(500);
record.setLabel("Admin User");
record.setYearsActive(5);
record.setRegistrationDate(new Date());
record.setActiveStatus(true);
Vehicle associatedVehicle = new Vehicle();
associatedVehicle.setModelName("Luxury Sedan");
associatedVehicle.setCost(45000.00);
record.setPrimaryVehicle(associatedVehicle);
return record;
}
}
During execution, the AbstractJackson2HttpMessageConverter handles the serialization process. It utilizes a JsonGenerator to write the object fields into the output stream, which is then sent to the client.
Content Negotiation Strategies
Content negotiation allows the server to return different media types based on the client's capabilities, typically indicated by the Accept header in the HTTP request.
Mechanism Overview
- If the client sends
Accept: application/json, the server responds with JSON. - If the client sends
Accept: application/xml, the server responds with XML.
Enabling XML Support
To support XML responses, add the Jackson XML dependency to the project configuration.
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.15.0</version>
</dependency>
Browser Behavior and Accept Headers
When accessing endpoints via a standard web browser, the response might default to XML instead of JSON. This occurs because browsers often send an Accept header prioritizing application/xhtml+xml or application/xml with a higher quality factor (q-value) than */*.
Example Header:
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Since application/xml has a weight of 0.9 compared to 0.8 for others, Spring Boot selects the XML message converter.
Parameter-Based Negotiation
To override browser header behavior, enable content negotiation based on request parameters. This allows clients to specify the desired format via a query parameter.
Configuration:
spring:
mvc:
contentnegotiation:
favor-parameter: true
parameter-name: formatType
With this configuration, appending ?formatType=json to the URL forces a JSON response, while ?formatType=xml forces XML. The parameter name defaults to format but can be customized as shown above. Ensure the specified format corresponds to a registered HttpMessageConverter.