How to Implement Content Negotiation with JSON and XML in Python
Build an HTTP server that returns JSON or XML responses based on the client's Accept header, with a 406 response for unsupported formats.
Python code
33 linesimport json
import xml.etree.ElementTree as ET
from http.server import BaseHTTPRequestHandler, HTTPServer
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
data = {"message": "Hello, world!"}
accept_header = self.headers.get("Accept", "")
if "application/json" in accept_header:
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps(data).encode())
elif "application/xml" in accept_header:
self.send_response(200)
self.send_header("Content-Type", "application/xml")
self.end_headers()
root = ET.Element("response")
ET.SubElement(root, "message").text = data["message"]
self.wfile.write(ET.tostring(root, encoding="utf-8"))
else:
self.send_response(406)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"Not Acceptable")
if __name__ == "__main__":
server = HTTPServer(("localhost", 8000), RequestHandler)
print("Server running on http://localhost:8000")
server.serve_forever()
Output
# Server log (running in terminal)
Server running on http://localhost:8000
127.0.0.1 - - [01/Jan/2025 12:00:00] "GET / HTTP/1.1" 200 -
# With Accept: application/json (using curl or browser)
# Response body:
{"message": "Hello, world!"}
# With Accept: application/xml (using curl or browser)
# Response body:
<response><message>Hello, world!</message></response>
# With Accept: text/plain (unsupported)
# Response status: 406 Not Acceptable
# Response body:
Not Acceptable
How it works
The handler reads the Accept header from the incoming request to determine the preferred response format. When the header contains application/json, the payload is serialized with json.dumps and returned with the matching Content-Type. For application/xml, an ElementTree root is built and serialized via ET.tostring, producing a well-formed XML document. If the client requests an unsupported format, a 406 Not Acceptable status is returned to signal that negotiation failed. This pattern mirrors how real REST APIs negotiate between multiple serialization formats dynamically.
Common mistakes
- Comparing the entire Accept header with `==` instead of using `in` to check for a substring — headers often contain multiple formats separated by commas.
- Forgetting to encode the response body with `.encode()` or using UTF-8 for XML serialization, leading to Unicode errors.
- Returning `200` with the wrong `Content-Type` for the actual body format, which confuses clients and breaks downstream parsing.
- Not handling the case of an empty or missing `Accept` header, defaulting to a specific format instead of returning 406.
Variations
- Use a dict-based dispatch with a `format_map` that maps MIME types to serializer functions for cleaner extensibility.
- Add support for `application/x-www-form-urlencoded` or `text/csv` by extending the if/elif chain with additional serializers.
Real-world use cases
- Building a REST API endpoint where mobile clients prefer JSON but legacy desktop clients require XML for backward compatibility.
- Implementing an HTTP service that serves configuration exports in both JSON and XML for different enterprise integration tools.
- Creating a mock server for testing API client behavior across multiple response formats during integration test suites.
Sponsored
More from API design & gRPC
- Build a Bulk Array POST Mock Server in Python medium
- Build a Mock REST API with PUT and GET in Python medium
- Convert Protobuf to JSON and Dict in Python easy
- Create a Data Helper in Python for gRPC-style APIs easy
- Format data in Python using dataclasses like gRPC messages easy
- Generate an OpenAPI Spec from Mock Routes in Python easy
Keep learning
Related tutorials and quizzes for this topic.