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 |
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:
-
Client says what it wants (via request headers)
-
Server chooses the best format it can provide
-
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!