Every Go APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. you'll build (or review from AI) encodes and decodes JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it.. The encoding/json package is one of AI's strongest areas, it generates correct marshaling code almost every time. Where things break is error handling: AI-generated code often ignores malformed input, missing fields, and type mismatches that happen constantly in production.
Your job: know enough to spot when AI's JSON handling is incomplete.
The two core operations
// Encode: Go struct -> JSON bytes
data, err := json.Marshal(user)
// Decode: JSON bytes -> Go struct
var user User
err := json.Unmarshal(jsonBytes, &user)That's the entire APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. surface for 90% of JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. work. Everything else is struct tags and edge cases.
Struct tags: controlling the JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. shape
When you ask AI to build a JSON APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses., it should generate struct tags. If it doesn't, the JSON keys will be capitalized Go field names, almost never what an API expects.
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"` // Omit if empty string
Password string `json:"-"` // Never serialize
IsAdmin bool `json:"is_admin"`
}| Tag option | What it does | Example output |
|---|---|---|
"name" | Custom JSON key | "name": "Alice" |
"-" | Exclude completely | Field never appears |
",omitempty" | Skip if zero value | Empty strings, 0, nil omitted |
",string" | Encode number as string | "age": "28" (quoted) |
json:"-" correctly for passwords, but sometimes forgets it on internal fields like PasswordHash, InternalID, or CreatedBy. When you review an AI-generated struct, check every field: "Should this be in the API response?" Leaked internal fields are a real security issue.Decoding JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. from external sources
This is where AI code breaks in production. AI generates the happy path, valid JSON, correct types, all fields present. Reality is different.
What AI generates
func handleRequest(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
json.NewDecoder(r.Body).Decode(&req) // No error check!
// ... use req
}What you actually need
func handleRequest(w http.ResponseWriter, r *http.Request) {
// Limit body size to prevent memory exhaustion
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1MB
var req CreateUserRequest
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields() // Reject typos in field names
if err := decoder.Decode(&req); err != nil {
http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
return
}
// Validate required fields
if req.Name == "" || req.Email == "" {
http.Error(w, "name and email are required", http.StatusBadRequest)
return
}
}http.MaxBytesReader. Without it, a malicious client can POST a 10GB body and exhaust your server's memory. Always limit request body size in HTTP handlers.The three decode failures AI ignores
| Failure | What happens | How to catch it |
|---|---|---|
| Malformed JSON | Decode returns error | Check the error (AI often doesn't) |
| Wrong types (string where int expected) | Decode returns error | Check the error |
| Missing required fields | Fields get zero values silently | Validate after decoding |
That third one is the killer. json.Unmarshal never errors on missing fields, it just leaves them as zero values. A missing "email" field becomes "", not an error. You must validate manually.
Dynamic JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. with map[string]any
When you don't know the JSON shape (third-party APIs, polymorphic responses), use map[string]any:
var result map[string]any
json.Unmarshal(data, &result)
// Access nested data with type assertions
status := result["status"].(string)
// Safe type assertion (won't panic)
if name, ok := result["name"].(string); ok {
fmt.Println(name)
}result["data"].(map[string]any) which panic if the key doesn't exist or the type is wrong. Always use the two-value form: v, ok := result["key"].(Type). One missing field in a third-party API response shouldn't crash your server.Streaming JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. with Encoder/Decoder
For HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. handlers, AI should use json.NewEncoder(w) and json.NewDecoder(r.Body) instead of Marshal/Unmarshal. They work directly with streams, no intermediate []byte.
// Writing JSON responses
func respondJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
// Reading JSON requests
func decodeJSON(r *http.Request, dst any) error {
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
return decoder.Decode(dst)
}Streaming large JSON arrays
For large datasets, streamWhat is stream?A way to process data in small chunks as it arrives instead of loading everything into memory at once, keeping memory usage low for large files. one element at a time instead of loading everything into memory:
file, _ := os.Open("large-data.json")
defer file.Close()
decoder := json.NewDecoder(file)
decoder.Token() // Read opening '['
for decoder.More() {
var item DataItem
decoder.Decode(&item)
process(item) // Handle one at a time
}Pointer fields for optional values
This is subtle but important. In Go, a missing JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. field and a field set to its zero value look identical after unmarshaling:
type Update struct {
Name string `json:"name"` // "" means missing OR intentionally empty
Name2 *string `json:"name2"` // nil means missing, "" means intentionally empty
}| JSON input | Name value | Name2 value |
|---|---|---|
{} | "" | nil |
{"name": ""} | "" | , |
{"name2": ""} | , | "" (pointer to empty string) |
{"name2": "Alice"} | , | "Alice" (pointer to "Alice") |
This matters for PATCH endpoints where you need to distinguish "field not sent" from "field explicitly set to empty."
string fields, which means you can't tell if a client sent "" intentionally or just didn't include the field. For update operations, prompt AI specifically: "Use pointer fields so I can distinguish missing from empty."Custom marshalers
Sometimes you need custom serializationWhat is serialization?Converting data from a program's internal format into a string or byte sequence that can be stored or sent over a network., dates in a specific format, enums as strings, computed fields. Implement json.Marshaler and json.Unmarshaler:
type Date struct {
time.Time
}
func (d Date) MarshalJSON() ([]byte, error) {
return json.Marshal(d.Format("2006-01-02"))
}
func (d *Date) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
t, err := time.Parse("2006-01-02", s)
if err != nil {
return err
}
d.Time = t
return nil
}AI generates custom marshalers correctly most of the time. The main thing to verify: the Unmarshal method should handle malformed input gracefully, not panic.