Restful Content Negotiation
title: "RESTful Content Negotiation — JSON, XML, and Custom Media Types" description: "RESTful content negotiation lets clients specify desired response format via Accept and Content-Type headers, supporting JSON, XML, and vendor-specific media types." date: 2026-06-28 lastmod: 2026-06-28 weight: 14 tags: [apis, restful] }
RESTful content negotiation allows clients to request specific response formats through HTTP Accept and Content-Type headers, supporting multiple representations of the same resource.
What You'll Learn
- Content negotiation via Accept header
- Supporting multiple formats
- Vendor-specific media types
Why It Matters
Content negotiation decouples resource identity from representation. The same URI serves JSON, XML, or custom formats based on client preferences.
Code Examples
from flask import request, jsonify, make_response
import dicttoxml
@app.route('/users')
def get_users():
# Determine response format
accept = request.headers.get('Accept', 'application/json')
users_data = [u.to_dict() for u in db.get_users()]
if 'application/xml' in accept or 'text/xml' in accept:
xml_response = dicttoxml.dicttoxml(users_data, custom_root='users')
response = make_response(xml_response)
response.headers['Content-Type'] = 'application/xml'
return response
# Default: JSON
response = jsonify(users_data)
response.headers['Content-Type'] = 'application/json'
return response
# Vendor-specific media types
@app.route('/users/<int:id>')
def get_user(id):
accept = request.headers.get('Accept', 'application/json')
user = db.get_user(id)
user_data = user.to_dict()
if 'application/vnd.api.v2+json' in accept:
# v2 format with additional fields
user_data['full_name'] = f"{user.first_name} {user.last_name}"
user_data['email'] = user.email
content_type = 'application/vnd.api.v2+json'
else:
# v1 format
user_data['name'] = user.first_name
content_type = 'application/vnd.api.v1+json'
response = jsonify(user_data)
response.headers['Content-Type'] = f'{content_type}; charset=utf-8'
response.headers['Vary'] = 'Accept'
return response
// Express content negotiation
app.get('/api/users', (req, res) => {
const users = db.getUsers();
res.format({
'application/json': () => {
res.json(users);
},
'application/xml': () => {
res.type('application/xml');
res.send(xmlBuilder.buildObject({ users }));
},
'default': () => {
res.status(406).json({ error: 'Not acceptable' });
}
});
});
Common Mistakes
1. Ignoring Accept Header
Returning JSON when client asked for XML.
2. Not Returning 406
Return 406 Not Acceptable when format is unsupported.
3. No Vary: Accept Header
Caches serve wrong format without Vary header.
4. File Extensions Instead of Negotiation
/users.json and /users.xml duplicate endpoints.
5. Case-Sensitive Format Comparison
Media types are case-insensitive. Compare lowercase.
Practice Questions
- What header does the client send to request a format?
- What status code for unsupported media types?
- Why use Vary: Accept?
- What is a vendor media type?
- How do you support multiple formats in one endpoint?
Answers:
- Accept header.
- 406 Not Acceptable.
- So caches store separate copies per Accept header value.
- A custom media type like
application/vnd.company.v1+json. - Parse Accept header and return the best matching format.
Challenge: Add XML support to your JSON API using content negotiation. Ensure Vary: Accept is set for proper caching.
FAQ
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro