Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
Most real APIs in 2026 are JSON-over-HTTP. You accept a JSON body, parse it, validate it, do your work, and return a JSON response. The failure mode you'll see most often is: someone sent you malformed JSON, or JSON with the wrong shape, and your handler crashed. Defining your request shape (a struct or schema) and validating every incoming request is the single most valuable habit to form on day one.
Accepting a JSON body means three distinct steps: parsing the bytes as JSON, validating the shape (the right fields, the right types), and acting on the result. Every framework handles each differently — some validate at the type level, some require explicit middleware. Returning a consistent envelope ({ ok: true, data: ... } vs { ok: false, error: ... }) makes every caller's job easier and makes your API debuggable at a glance.
curl -X POST http://localhost:8080/users -H 'content-type: application/json' -d '{"name":"Ada","email":"a@b.com"}'. See 201.-d '{not json'. See 400.-d '{}'. See 422.-d '{"name":"Ada","email":"nope"}'. In Python (pydantic) and Node (zod) this is 422. In Go it's 201 (manual validation didn't check format). Note the difference.Use these three in order. Each builds on the one before.
Explain why every JSON API needs request validation. What are the failure modes if you skip it?
Pydantic (Python), zod (TypeScript), and serde (Rust) all do schema validation, but they work very differently internally. Walk me through each one's approach — runtime parsing vs compile-time derivation — and the performance implications.
Your API needs to return validation errors to a frontend in a way the frontend can render field-by-field ('email is invalid', 'name is too long'). Design the error response shape, and explain how pydantic/zod/serde each let you produce that shape.
package main
import (
"encoding/json"
"net/http"
)
type CreateUser struct {
Name string `json:"name"`
Email string `json:"email"`
}
func main() {
http.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
var body CreateUser
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "invalid JSON: "+err.Error(), 400)
return
}
if body.Name == "" || body.Email == "" {
http.Error(w, "name and email are required", 422)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(201)
json.NewEncoder(w).Encode(map[string]any{
"ok": true,
"user": body,
})
})
http.ListenAndServe(":8080", nil)
}go run main.go