Writing JSON Responses

Send a JSON response correctly: marshal first, set Content-Type, then the status, then the body.

Syntaxw.Header().Set("Content-Type", "application/json"); w.WriteHeader(code); w.Write(body)

A response has three parts and they leave the server in a fixed order: headers, then a status code, then the body. Go lets you write them in any order in your code, and quietly drops the ones that arrive too late. Getting that order right is most of this lesson.

The order

w.Header() returns a map of header names to values. Set Content-Type to application/json there, so the client knows how to read what follows. Then call w.WriteHeader with the status code. Then write the body. WriteHeader is the point of no return. Once a 2xx to 5xx status has been sent the server has taken its own copy of the header map, and anything you set afterwards goes nowhere.

Marshal before you commit

json.Marshal turns a Go value into bytes and returns an error alongside them. json.NewEncoder(w).Encode(v) is the shorthand for the same work: it converts and writes in one call, and it appends a newline. The difference shows up on failure. Encode writes straight to the client, so if conversion fails partway the status is already 200 and the body is cut off. Converting into a byte slice first leaves you free to send a 500 instead.

What trips people up

If you never call WriteHeader, the first Write sends 200 on your behalf. So a WriteHeader(http.StatusCreated) placed after an Fprintf does nothing at all, and the server logs a complaint about a superfluous call.

Example

Example · go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
)

type book struct {
	Title string `json:"title"`
	Pages int    `json:"pages"`
}

// writeJSON converts first, then sets headers, then the status, then the body.
func writeJSON(w http.ResponseWriter, status int, v any) {
	buf, err := json.Marshal(v)
	if err != nil {
		// Nothing has been sent yet, so a 500 is still possible here.
		http.Error(w, "could not encode response", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_, _ = w.Write(buf) // a write error means the client left; nothing to do
}

func main() {
	rec := httptest.NewRecorder()
	writeJSON(rec, http.StatusOK, book{Title: "Dune", Pages: 412})
	fmt.Println(rec.Code)
	fmt.Println(rec.Header().Get("Content-Type"))
	fmt.Print(rec.Body.String())
}

// Output:
// 200
// application/json
// {"title":"Dune","pages":412}

When to use it

  • Write one writeJSON helper that every handler in a service uses, so content type and status order are correct in one place.
  • Return a created record as JSON with 201 and still be able to fall back to a 500 if encoding fails.
  • Debug a client that reports text/plain responses from an endpoint you believe sends JSON.

More examples

The encoder shorthand, and what it changes

Use the encoder when the value is large or streamed, so you never hold the whole response in memory. SetEscapeHTML(false) turns off the default escaping of HTML-significant characters, which is only needed when the JSON will be embedded in a page. Note what you give up: the status has already gone out, so an encoding failure can only be logged.

Example · go
// Shorthand: converts and writes in one call, and adds a trailing "\n".
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
enc := json.NewEncoder(w)
enc.SetEscapeHTML(false) // keep <, > and & as themselves, not < etc.
enc.SetIndent("", "  ") // pretty-print; costs bytes, useful for a debug endpoint
if err := enc.Encode(b); err != nil {
	// Too late for a status. Log it and move on.
	log.Printf("encode response: %v", err)
}

The header-after-write mistake, side by side

The bad version compiles, passes a test that only checks the body, and ships. The client then receives 200 with Content-Type: text/plain; charset=utf-8, because Go sniffed the body. When a response has the wrong content type or a stubbornly wrong status, look for a write above the header lines.

Example · go
// Wrong: the body goes first, which sends 200 and clears the header map.
func badHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, `{"ok":true}`)
	w.Header().Set("Content-Type", "application/json") // ignored
	w.WriteHeader(http.StatusCreated)                  // ignored, and logged
}

// Right: headers, then status, then body.
func goodHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	fmt.Fprint(w, `{"ok":true}`)
}

A response with no body at all

Reach for 204 on a successful DELETE, or a PUT whose result the client already has. Sending {} with a 200 instead is harmless but wasteful, and some clients will try to parse an empty body as JSON and fail. Setting Content-Type here would be misleading, since nothing follows.

Example · go
func deleteBook(w http.ResponseWriter, r *http.Request) {
	if err := store.Delete(r.PathValue("id")); err != nil {
		http.Error(w, "could not delete", http.StatusInternalServerError)
		return
	}
	// 204 means "it worked, there is nothing to send". No Content-Type,
	// no body: a body on a 204 is a protocol error.
	w.WriteHeader(http.StatusNoContent)
}

Discussion

  • Be the first to comment on this lesson.