Introduction
Most Go web services communicate using JSON. Go's encoding/json package provides Encoder and Decoder types that stream JSON directly to and from HTTP bodies, making it efficient and straightforward.
Key Concepts
- json.NewEncoder(w).Encode(v): Writes a Go value as JSON directly to the
ResponseWriterstream. - json.NewDecoder(r.Body).Decode(&v): Reads JSON from the request body and populates a Go struct.
- Struct tags: Annotations like
json:"name"control how struct fields map to JSON keys. - http.MaxBytesReader: Wraps the request body to limit its size, preventing denial-of-service via oversized payloads.
Real World Context
Every REST API needs to serialize responses and deserialize requests. Using json.Encoder/Decoder (streaming) instead of json.Marshal/Unmarshal (buffered) avoids unnecessary memory allocations, especially important under high traffic.
Deep Dive
Sending a JSON response requires setting the Content-Type header and encoding the value.
gofunc getUser(w http.ResponseWriter, r *http.Request) { user := User{ID: 1, Name: "Alice"} w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(user) }
The encoder writes directly to the ResponseWriter, avoiding an intermediate byte buffer.
Parsing a JSON request body follows a similar pattern with the decoder.
gofunc createUser(w http.ResponseWriter, r *http.Request) { var user User if err := json.NewDecoder(r.Body).Decode(&user); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // Validate and process user... }
Always check the decode error — malformed JSON returns a descriptive error message.
For consistent error responses, define an error struct and a helper function.
gotype ErrorResponse struct { Error string `json:"error"` Code string `json:"code,omitempty"` Details any `json:"details,omitempty"` } func writeError(w http.ResponseWriter, status int, msg string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(ErrorResponse{Error: msg}) }
This ensures every error response has the same shape, making client-side error handling predictable.
Limit request body size to prevent abuse.
gor.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1MB limit
If the body exceeds the limit, subsequent reads return an error.
Common Pitfalls
- Forgetting
Content-Type: application/json— Without this header, clients may not parse the response as JSON, leading to confusing errors. - Using
json.Unmarshalinstead ofjson.NewDecoder—Unmarshalrequires reading the entire body into memory first withio.ReadAll, which is wasteful for large payloads.
Best Practices
- Always limit request body size — Use
http.MaxBytesReaderto prevent memory exhaustion from oversized requests. - Define a consistent error response struct — A uniform error shape across all endpoints simplifies client error handling.
Summary
- Use
json.NewEncoder(w).Encode()for responses andjson.NewDecoder(r.Body).Decode()for requests. - Always set
Content-Type: application/jsonbefore writing. - Limit body size with
http.MaxBytesReaderto prevent abuse.
Code Examples
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /users", func(w http.ResponseWriter, r *http.Request) {
users := []User{{ID: 1, Name: "Alice", Email: "alice@example.com"}}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
})
mux.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
})
http.ListenAndServe(":8080", mux)
}