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


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.

StateHeaderPurpose
HTTP RequestAcceptIdentified by the client’s Accept header, which specifies the response type the client wants.
HTTP ResponseContent-TypeIdentified 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 Accept header approach. If the client requests Accept: application/xml and the required XML library is present, Spring automatically selects the appropriate HttpMessageConverter (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 via WebMvcConfigurer.
      • However, relying on the Accept header is the REST standard and is recommended.

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 for Accept: application/xml header, Spring will internally check for the jackson-dataformat-xml API dependency. If found, the Java bean will be automatically transformed to XML using the MappingJackson2XmlHttpMessageConverter.


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 HeaderExpected Response Format
Accept: application/jsonJSON
Accept: application/xmlXML

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 Accept header 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 Accept header 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 the Accept header, which is where the 406 response 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 406 is 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-Type header for the response. If it cannot, the 406 is 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:

Feature406 Not Acceptable415 Unsupported Media Type
Request PartAccept header (Client –> Server)Content-Type header (Client –> Server)
Flow DirectionResponse Content (What the server sends back)Request Content (What the client sends to the server)
MeaningThe 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.
ExampleClient 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-Type header.
  • The Error: The client is sending data formatted as X (Content-Type: X), but the server endpoint only has a deserializer (an HttpMessageConverter) registered to read data formatted as Y. The server rejects the input immediately.
  • In Spring Boot: This commonly occurs in a POST or PUT request where:
    • The controller method expects a JSON body (the default).
    • The client sends a request with the header Content-Type: text/plain and a non-text body. Spring fails to find a converter to turn the plain text into the required Java object, resulting in a 415.

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 Accept header.
  • 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-xml to handle Accept: application/xml) and the server is configured to strictly enforce content negotiation.

In short:
415 is about the input format (what the server can consume).
406 is 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 Accept header is the standard and preferred way. Avoid mixing URL suffixes or query parameters unless absolutely necessary, as it clutters the API design.
  • Fallback Mechanism:
    • Know your server’s default type. If the client sends an Accept header that the server doesn’t support, the server will usually respond with the default format (typically JSON) and a Content-Type header indicating the actual format, or a 406 Not Acceptable status if explicitly configured to enforce the Accept header.
  • 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.