HTTP Topic

Content Negotiation and Media Types

What You'll Learn:

  • Understanding MIME types and content negotiation

  • Supporting multiple response formats (JSON, XML, etc.)

  • Versioning through content types

Understanding MIME Types and Content Negotiation

What are MIME types?

MIME (Multipurpose Internet Mail Extensions) types describe what type of data a server sends or receives.

Examples:

File Type MIME Type
JSON application/json
XML application/xml
HTML text/html
PNG Image image/png
PDF application/pdf

When a client makes a request, it can specify what media type it wants.

How does content negotiation work?

Content negotiation is the process of:

  1. Client says what it wants (via request headers)

  2. Server chooses the best format it can provide

  3. Server returns the response in that format

Clients express this using:

Accept Header

Example:

Accept: application/json

Or multiple options:

Accept: application/json, application/xml;q=0.8

q means priority (quality factor). Higher = more preferred.

Example Flow

Client →

GET /api/products Accept: application/xml

Server →
Checks supported formats → Responds with XML.


Supporting Multiple Response Formats (JSON, XML, etc.)

APIs can support multiple output formats by:

  • Detecting the Accept header from the client

  • Serializing the response accordingly

JSON Example

Response header:

Content-Type: application/json

Response body:

{ "name": "Laptop", "price": 999 }

XML Example

Response header:

Content-Type: application/xml

Response body:

<Product> <Name>Laptop</Name> <Price>999</Price> </Product>

In frameworks like Flask, Django REST Framework, ASP.NET Web API, or Spring:

  • You can plug in serializers

  • APIs automatically format output based on Accept headers

  • You can manually override formatting if needed

Why support multiple formats?

  • Mobile apps might prefer JSON

  • Legacy systems might expect XML

  • Different departments/clients might have different needs


Versioning Through Content Types

API versioning ensures that new changes don’t break old clients.

One method is using media type versioning, also known as MIME type versioning.

Example of versioned media types

Accept: application/vnd.myapp.v1+json

or

Accept: application/vnd.myapp.v2+xml

Breaking it down:

  • application/vnd.myapp → vendor-specific media type

  • v1 → API version

  • +json → response format

Example Requests

Client requesting version 1:

Accept: application/vnd.shop.v1+json

Client requesting version 2:

Accept: application/vnd.shop.v2+json

Server can then:

  • Route the request to the correct version’s controller/serializer

  • Return updated fields only for newer versions

  • Maintain backward compatibility

Why use content-type versioning?

✔ Does not clutter URLs (e.g., /v1/products)
✔ Allows multiple representations under the same endpoint
✔ Common in large enterprise APIs

Detailed Explanation:

MIME Types (Media Types)

Common Media Types:

# JSON
Content-Type: application/json
Content-Type: application/json; charset=utf-8

# XML
Content-Type: application/xml
Content-Type: text/xml

# Form Data
Content-Type: application/x-www-form-urlencoded
Content-Type: multipart/form-data; boundary=something

# Binary Data
Content-Type: application/octet-stream
Content-Type: image/jpeg
Content-Type: application/pdf

Custom API Media Types:

# Versioned JSON
Content-Type: application/vnd.company.user.v1+json
Content-Type: application/vnd.company.user.v2+json

# Hypermedia API
Content-Type: application/vnd.company.hal+json

Content Negotiation Headers

Client Request Headers:

# Accept multiple formats with quality factors
Accept: application/json, application/xml;q=0.9, text/plain;q=0.8

# Accept specific character set
Accept-Charset: utf-8, iso-8859-1;q=0.5

# Accept language
Accept-Language: en-US, en;q=0.9, fr;q=0.7

# Accept encoding
Accept-Encoding: gzip, deflate, br

Server Response Headers:

# Content type actually served
Content-Type: application/json; charset=utf-8

# Available content types for this resource
Vary: Accept, Accept-Language

# Content encoding
Content-Encoding: gzip

Spring Boot Content Negotiation

Configuration:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer
            .favorParameter(true)
            .parameterName("format")
            .ignoreAcceptHeader(false)
            .useRegisteredExtensionsOnly(false)
            .defaultContentType(MediaType.APPLICATION_JSON)
            .mediaType("json", MediaType.APPLICATION_JSON)
            .mediaType("xml", MediaType.APPLICATION_XML)
            .mediaType("hal", MediaType.parseMediaType("application/hal+json"));
    }
}

Controller Implementation:

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    // Automatic content negotiation based on Accept header
    @GetMapping(value = "/{id}", produces = {
        MediaType.APPLICATION_JSON_VALUE,
        MediaType.APPLICATION_XML_VALUE,
        "application/hal+json"
    })
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
    
    // Versioned content types
    @GetMapping(value = "/{id}", 
                produces = "application/vnd.company.user.v1+json")
    public UserV1 getUserV1(@PathVariable Long id) {
        return userService.findV1ById(id);
    }
    
    @GetMapping(value = "/{id}", 
                produces = "application/vnd.company.user.v2+json")
    public UserV2 getUserV2(@PathVariable Long id) {
        return userService.findV2ById(id);
    }
}

Custom Message Converters

Hal+JSON Converter:

@Component
public class HalMessageConverter extends AbstractHttpMessageConverter<Object> {
    
    public HalMessageConverter() {
        super(MediaType.parseMediaType("application/hal+json"));
    }
    
    @Override
    protected boolean supports(Class<?> clazz) {
        return true; // Support all types
    }
    
    @Override
    protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) 
            throws IOException, HttpMessageNotReadableException {
        // Implement deserialization
        return null;
    }
    
    @Override
    protected void writeInternal(Object object, HttpOutputMessage outputMessage) 
            throws IOException, HttpMessageNotWritableException {
        
        Object halResource = convertToHal(object);
        String json = objectMapper.writeValueAsString(halResource);
        
        outputMessage.getBody().write(json.getBytes(StandardCharsets.UTF_8));
    }
}

Versioning Strategies

1. URL Versioning:

GET /api/v1/users/123
GET /api/v2/users/123

2. Query Parameter Versioning:

GET /api/users/123?version=1
GET /api/users/123?version=2

3. Header Versioning:

GET /api/users/123
API-Version: 1

4. Content Type Versioning:

GET /api/users/123
Accept: application/vnd.company.user.v1+json

GET /api/users/123
Accept: application/vnd.company.user.v2+json

Response Format Examples

JSON Response:

{
  "id": 123,
  "name": "John Doe",
  "email": "john@example.com",
  "createdAt": "2023-01-23T10:30:45Z"
}

XML Response:

<user>
  <id>123</id>
  <name>John Doe</name>
  <email>john@example.com</email>
  <createdAt>2023-01-23T10:30:45Z</createdAt>
</user>

HAL+JSON Response:

{
  "id": 123,
  "name": "John Doe",
  "email": "john@example.com",
  "_links": {
    "self": { "href": "/api/users/123" },
    "orders": { "href": "/api/users/123/orders" }
  },
  "_embedded": {
    "address": {
      "street": "123 Main St",
      "city": "Anytown"
    }
  }
}

Advanced Content Negotiation

Custom Media Type with Parameters:

@GetMapping(value = "/{id}", produces = "application/json; version=1")
public UserV1 getUserV1(@PathVariable Long id) {
    return userService.findV1ById(id);
}

@GetMapping(value = "/{id}", produces = "application/json; version=2")
public UserV2 getUserV2(@PathVariable Long id) {
    return userService.findV2ById(id);
}

Language Negotiation:

@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id,
                                   @RequestHeader("Accept-Language") String acceptLanguage) {
    
    Locale locale = Locale.lookup(Locale.LanguageRange.parse(acceptLanguage), 
                                 Arrays.asList(Locale.US, Locale.FRANCE));
    
    User user = userService.findById(id);
    User localizedUser = localizeUser(user, locale);
    
    return ResponseEntity.ok()
        .contentType(MediaType.APPLICATION_JSON)
        .header("Content-Language", locale.toLanguageTag())
        .body(localizedUser);
}

 

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?