Response Content Negotiation
👉 Handling JSON, XML, and Custom Response Types in controller and Resolving HTTP 406/415 Errors in Spring Boot
What is Content Negotiation ?
Content negotiation is a crucial mechanism in web services, allowing a single resource to be available in multiple representations (e.g., JSON, XML). It primarily involves the client and server agreeing on the format for exchanging data.
Table of Contents
- What is Content Negotiation ?
- Table of Contents
- How content negition works ?
- Maven Dependency
- Application Configuration
- Content Transformation Flow
- HTTP 406 Not Acceptable
- How to enforce strict content negotiation in Spring Boot ?
- Testing the HTTP 406 Scenario
- HTTP Error - 406 vs. 415
- Takeaway
How content negition works ?
The client sends an HTTP headerAccept (e.g., Accept: application/json or Accept: application/xml) in its request, indicating the media types it can process.
The server examines this header and attempts to return the resource in one of the requested formats. If no preferred format is specified by the client, the server typically defaults to a predetermined format, often JSON.
| State | Header | Purpose |
|---|---|---|
| HTTP Request | Accept | Identified by the client’s Accept header, which specifies the response type the client wants. |
| HTTP Response | Content-Type | Identified by the server’s Content-Type header in the response, which specifies the actual format of the data being returned. |
How to configure content negotion ?
Spring Boot, leveraging Spring Web MVC, makes content negotiation largely automatic through its HttpMessageConverter mechanism.
- Supported Content Types:
- Spring Boot natively supports many formats. For converting Java objects to and from data formats, the primary types are JSON (via Jackson) and XML.
- Support for other types like plain text or byte streams is also available. To support a new format like XML, you just need to add the appropriate dependency; Spring Boot handles the wiring.
Automatic vs. Manual Config:
- Automatic: In most cases, it’s automatic. By default, Spring Boot uses the
Acceptheader approach. If the client requestsAccept: application/xmland the required XML library is present, Spring automatically selects the appropriateHttpMessageConverter(e.g.,MappingJackson2XmlHttpMessageConverter) to serialize the Java object to XML. - Manual/Custom: You can manually configure it, for example, by using URL suffixes (
/resource.xml), query parameters (/resource?format=xml), or overriding the default converters and strategies viaWebMvcConfigurer.- However, relying on the
Acceptheader is the REST standard and is recommended.
- However, relying on the
- Automatic: In most cases, it’s automatic. By default, Spring Boot uses the
Maven Dependency
In our application, we need to ensure that the server can handle XML serialization in addition to the default JSON.
Add the following dependency to your pom.xml file. This library is required for converting Java objects to XML format.
<!-- pom.xml -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
Note:
If the client requests forAccept: application/xmlheader, Spring will internally check for thejackson-dataformat-xmlAPI dependency. If found, the Java bean will be automatically transformed to XML using theMappingJackson2XmlHttpMessageConverter.
Application Configuration
The beauty of Spring Boot’s content negotiation is that no code changes are required in the controller for this basic functionality. The core logic resides in a simple REST Controller that returns a standard Java object.
Model Class: Product
package com.example.model;
public class Product {
private Long id;
private String name;
private double price;
// Constructors, Getters, and Setters
public Product() {}
public Product(Long id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
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
// ProductController.java
package com.example.controller;
import com.example.model.Product;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ProductController {
@GetMapping("/api/product")
public Product getProduct() {
// This single method can return JSON or XML based on the Accept header
return new Product(101L, "Laptop", 1200.50);
}
}
Project Reference:
Check the project POC on Github - Content Negotiation
Testing with CURL command
These commands demonstrate how the client’s Accept header dictates the format of the server’s response (JSON or XML). Assume your Spring Boot application is running locally on port 8080.
| Client Request Header | Expected Response Format |
|---|---|
Accept: application/json | JSON |
Accept: application/xml | XML |
Requesting JSON Format
This is the standard request. We explicitly set the Accept header to application/json. The server should respond with a JSON payload.
# cURL command for requesting JSON
curl -X GET http://localhost:8080/api/product \
-H 'Accept: application/json' \
-i
Expected JSON Response Body:
{
"id": 101,
"name": "Laptop",
"price": 1200.5
}
Requesting XML Format
By changing the Accept header to application/xml, the Spring Boot application (with the jackson-dataformat-xml dependency) automatically serializes the Product object into XML.
# cURL command for requesting XML
curl -X GET http://localhost:8080/api/product \
-H 'Accept: application/xml' \
-i
Expected XML Response Body:
<Product>
<id>101</id>
<name>Laptop</name>
<price>1200.5</price>
</Product>
Content Transformation Flow
The following Mermaid diagram illustrates the automatic content negotiation flow in a Spring Boot application.
sequenceDiagram
participant C as Client
participant SB as Spring Boot Application
participant Disp as DispatcherServlet
participant Conv as HttpMessageConverter (e.g., Jackson JSON/XML)
C->>SB: HTTP GET /api/product (Header: Accept: application/xml)
SB->>Disp: Request received
Disp->>Disp: Finds @GetMapping("/api/product") handler
Disp->>SB: Calls ProductController.getProduct()
SB-->>Disp: Returns Product Java Object
Disp->>Disp: Determines return type is Product
Disp->>Disp: Checks client's Accept header (application/xml)
Disp->>Conv: Selects MappingJackson2XmlHttpMessageConverter
Conv->>Disp: Converts Product Object to XML String
Disp-->>SB: Returns XML Response
SB-->>C: HTTP 200 OK (Header: Content-Type: application/xml)
C->>SB: HTTP GET /api/product (Header: Accept: application/json)
SB->>Disp: Request received
Disp->>Disp: Checks client's Accept header (application/json)
Disp->>Conv: Selects MappingJackson2HttpMessageConverter
Conv->>Disp: Converts Product Object to JSON String
Disp-->>SB: Returns JSON Response
SB-->>C: HTTP 200 OK (Header: Content-Type: application/json)
HTTP 406 Not Acceptable
That’s a very practical and insightful! to understanding the 406 Not Acceptableerror is crucial for building robust APIs.
The 406 Not Acceptable HTTP status code is a direct outcome of failed content negotiation. It is a clear signal from the server to the client that while the server has found the resource, it cannot provide a representation of that resource that is acceptable to the client, based on the criteria specified in the request.
- Definition: The server understands the content type requested in the client’s
Acceptheader but does not have a resource converter or handler to generate a response in that specific format. - The Problem: The server cannot fulfill the requirement. For instance, the client might request
Accept: image/jpeg, but the Spring Boot endpoint is designed only to return a Java object that can be serialized into JSON or XML. - Spring Boot’s Default Behavior: By default, Spring Web MVC is quite lenient. If a request is made with an
Acceptheader for an unsupported type (e.g.,Accept: application/pdf), Spring often attempts to fall back to its primary supported format, which is JSON. However, you can configure Spring Boot to strictly enforce theAcceptheader, which is where the406response becomes necessary.
How to enforce strict content negotiation in Spring Boot ?
To ensure your API strictly adheres to the requested Accept header and returns a 406 when the format isn’t supported, you need to customize the ContentNegotiationConfigurer.
This can be achieved by implementing the WebMvcConfigurer interface and overriding the configureContentNegotiation method.
The key setting here is ignoreAcceptHeader(false) and defaultContentTypeStrategy(null) or similar strict settings.
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
// 1. Explicitly do NOT ignore the Accept header.
configurer.ignoreAcceptHeader(false)
// 2. Set the media types the server supports.
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xml", MediaType.APPLICATION_XML)
// 3. Prevent automatic fallback to a default content type (like JSON).
// If a requested type is not listed above, it will result in a 406.
.defaultContentType(MediaType.APPLICATION_JSON); // Set a default only if Accept header is missing.
}
}
Testing the HTTP 406 Scenario
Now that the application is configured to strictly support only application/json and application/xml, here is the cURL command that would trigger the 406 error.
Requesting Unsupported Format (e.g., YAML)
# cURL command requesting an unsupported format
curl -X GET http://localhost:8080/api/product \
-H 'Accept: application/yaml' \
-i
Expected Response Headers:
HTTP/1.1 406 Not Acceptable
Content-Type: application/problem+json
...
Developer Awareness
- Benefit of 406: Returning a
406is good API etiquette. It explicitly tells the client, “I can’t serve the format you asked for”, preventing the client from trying to parse an unexpected format (e.g., XML when it expected YAML). - Server Support: A server must support the media type it lists in its
Content-Typeheader for the response. If it cannot, the406is the appropriate response status.
HTTP Error - 406 vs. 415
These two status codes refer to negotiation failures but relate to different parts of the HTTP request-response cycle:
| Feature | 406 Not Acceptable | 415 Unsupported Media Type |
|---|---|---|
| Request Part | Accept header (Client –> Server) | Content-Type header (Client –> Server) |
| Flow Direction | Response Content (What the server sends back) | Request Content (What the client sends to the server) |
| Meaning | The server cannot generate a response representation that the client accepts, requested in the Accept header. | The server cannot process the request payload because the format specified in the Content-Type header is not supported. |
| Example | Client wants XML (Accept: application/xml), but the server can only produce JSON. | Client sends YAML data (Content-Type: application/yaml), but the server only accepts JSON for input. |
415 Unsupported Media Type (The Request Problem)
This code is used when the server cannot understand or process the data format provided in the request payload.
- Request Role: It is governed by the client’s
Content-Typeheader. - The Error: The client is sending data formatted as X (
Content-Type: X), but the server endpoint only has a deserializer (anHttpMessageConverter) registered to read data formatted as Y. The server rejects the input immediately. - In Spring Boot: This commonly occurs in a
POSTorPUTrequest where:- The controller method expects a JSON body (the default).
- The client sends a request with the header
Content-Type: text/plainand a non-text body. Spring fails to find a converter to turn the plain text into the required Java object, resulting in a415.
406 Not Acceptable (The Response Problem)
This code is used when the server cannot satisfy the client’s requirements for the response format.
- Request Role: It is governed by the client’s
Acceptheader. - The Error: The client is saying, “I will only read X,” but the server only knows how to write Y and Z. Since Y and Z are not X, the negotiation fails.
- In Spring Boot: This typically happens when the required serialization library is missing (e.g., no
jackson-dataformat-xmlto handleAccept: application/xml) and the server is configured to strictly enforce content negotiation.
In short:
415is about the input format (what the server can consume).406is about the output format (what the server can produce).
Takeaway
- Consistency is Key:
- While content negotiation allows for multiple formats, it’s often best practice in modern microservices to standardize on JSON unless there’s a specific requirement for XML (e.g., integration with legacy systems). JSON is lighter, faster, and universally supported by modern web tools.
- Header Priority:
- The
Acceptheader is the standard and preferred way. Avoid mixing URL suffixes or query parameters unless absolutely necessary, as it clutters the API design.
- The
- Fallback Mechanism:
- Know your server’s default type. If the client sends an
Acceptheader that the server doesn’t support, the server will usually respond with the default format (typically JSON) and aContent-Typeheader indicating the actual format, or a406 Not Acceptablestatus if explicitly configured to enforce theAcceptheader.
- Know your server’s default type. If the client sends an
- Version Control:
- Content negotiation can be used for API versioning (e.g.,
Accept: application/vnd.mycompany.v2+json), though managing versions via the URL is often clearer.
- Content negotiation can be used for API versioning (e.g.,