go 77 lines · 2 tabs

Content-Negotiating HTTP Handler for JSON and XML in Go

Shared by codesnips Sep 2026
2 tabs
package api

import (
	"encoding/json"
	"encoding/xml"
	"net/http"
	"strings"
)

const (
	mimeJSON = "application/json"
	mimeXML  = "application/xml"
)

func bestMatch(accept string) string {
	for _, part := range strings.Split(accept, ",") {
		media := strings.TrimSpace(part)
		if i := strings.IndexByte(media, ';'); i >= 0 {
			media = strings.TrimSpace(media[:i])
		}
		switch media {
		case mimeXML, "text/xml":
			return mimeXML
		case mimeJSON:
			return mimeJSON
		case "*/*", "":
			return mimeJSON
		}
	}
	return mimeJSON
}

func Respond(w http.ResponseWriter, r *http.Request, status int, payload interface{}) error {
	media := bestMatch(r.Header.Get("Accept"))
	w.Header().Set("Content-Type", media+"; charset=utf-8")
	w.WriteHeader(status)

	if media == mimeXML {
		return xml.NewEncoder(w).Encode(payload)
	}
	return json.NewEncoder(w).Encode(payload)
}
2 files · go Explain with highlit

This snippet shows how to build an HTTP handler in Go that inspects the client's Accept header and serializes the same response payload as either JSON or XML. Content negotiation is a core part of HTTP: the server exposes one resource, and the representation is chosen at request time based on what the client asks for. Rather than duplicating handler logic per format, the negotiation is factored into a small reusable helper so every endpoint stays format-agnostic.

In negotiate.go, the exported Respond function is the entry point. It parses the Accept header with bestMatch, which splits the header on commas, strips any ;q= quality parameters, and returns the first media type the server actually supports. The lookup is deliberately simple and preference-ordered: an explicit match on application/xml or text/xml picks XML, application/json picks JSON, and a wildcard */* or empty header falls back to a sensible default (application/json). This mirrors how most REST APIs behave and avoids the complexity of full RFC 7231 quality-value sorting while still handling the common cases correctly.

Once a media type is chosen, Respond sets the Content-Type header, writes the status code with WriteHeader, and encodes using either json.NewEncoder or xml.NewEncoder streamed directly to the http.ResponseWriter. Streaming the encoder avoids building an intermediate buffer. Note the ordering pitfall it sidesteps: headers and status must be written before any body bytes, because the first Write implicitly flushes a 200.

In handler.go, BookHandler demonstrates the payoff. The Book struct carries both json and xml struct tags, plus an XMLName field so XML output gets a proper root element. The handler builds its data once and calls Respond, never branching on format itself. renderError shows the same helper serializing an error envelope, so error responses honor negotiation too.

The trade-off is that this hand-rolled matcher ignores q weighting and less common media types; for richer needs a library handles the full grammar. But for a service that speaks JSON and XML, this keeps handlers clean, testable, and free of per-format duplication.


Related snips

Share this code

Here's the card — post it anywhere.

Content-Negotiating HTTP Handler for JSON and XML in Go — share card
Link copied