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.

Medium Python 3.9+ Aug 9, 2026 API design & gRPC 11 views 0 copies

Python code

33 lines
Python 3.9+
import 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

stdout
# 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

  1. Use a dict-based dispatch with a `format_map` that maps MIME types to serializer functions for cleaner extensibility.
  2. 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

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from API design & gRPC

Related tutorials and quizzes for this topic.