Go/
Lesson

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.

02

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 optionWhat it doesExample output
"name"Custom JSON key"name": "Alice"
"-"Exclude completelyField never appears
",omitempty"Skip if zero valueEmpty strings, 0, nil omitted
",string"Encode number as string"age": "28" (quoted)
AI pitfall
AI generates 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.
03

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
    }
}
AI pitfall
AI almost never adds 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

FailureWhat happensHow to catch it
Malformed JSONDecode returns errorCheck the error (AI often doesn't)
Wrong types (string where int expected)Decode returns errorCheck the error
Missing required fieldsFields get zero values silentlyValidate 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.

04

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)
}
AI pitfall
AI generates unsafe type assertions like 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.
05

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
}
06

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 inputName valueName2 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."

AI pitfall
AI almost never uses pointer fields for optional values in PATCH handlers. It generates 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."
07

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.