Day 17 — encoding/json
Learning Objectives
- Use streaming JSON decoding to avoid buffering large request bodies
- Write custom marshalers for sensitive fields (mask passwords in logs)
- Handle unknown fields for security —
json.Decoder.DisallowUnknownFields()
Key Concepts
// Struct tags control JSON field names and behavior
type AuthRequest struct {
Username string `json:"username"`
Password string `json:"password,omitempty"` // omit if empty
MFA string `json:"mfa_code,omitempty"`
}
// Decoding — use decoder, not Unmarshal, for streaming
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields() // reject {"username":"a", "admin":true}
if err := dec.Decode(&req); err != nil {
http.Error(w, "invalid request", 400)
return
}
// Custom marshaling — hide sensitive fields
type LogSafeAuthRequest struct {
Username string `json:"username"`
Password string `json:"-"` // never marshaled
}
// json.RawMessage — defer parsing of a field
type Event struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload"` // parse later based on Type
}
Exercise: Secure JSON API Pipeline
package main
import (
"bytes"
"encoding/json"
"fmt"
"strings"
)
// LoginRequest — incoming from client
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
TOTPCode string `json:"totp_code,omitempty"`
}
// Validate enforces field rules
func (r *LoginRequest) Validate() error {
if r.Username == "" {
return fmt.Errorf("username required")
}
if len(r.Username) > 64 {
return fmt.Errorf("username too long (max 64)")
}
if r.Password == "" {
return fmt.Errorf("password required")
}
if len(r.Password) < 8 {
return fmt.Errorf("password too short (min 8)")
}
return nil
}
// LoginResponse — sent to client
type LoginResponse struct {
Token string `json:"token"`
ExpiresIn int `json:"expires_in"` // seconds
}
// AuditEntry — logged (password masked)
type AuditEntry struct {
Username string `json:"username"`
Password string `json:"-"` // NEVER log
Action string `json:"action"`
Success bool `json:"success"`
Reason string `json:"reason,omitempty"`
}
func processLogin(body string) {
dec := json.NewDecoder(strings.NewReader(body))
dec.DisallowUnknownFields() // OWASP A03: reject parameter pollution
var req LoginRequest
if err := dec.Decode(&req); err != nil {
fmt.Printf("decode error: %v\n", err)
return
}
if err := req.Validate(); err != nil {
fmt.Printf("validation error: %v\n", err)
return
}
// Audit log — password field is omitted via json:"-"
audit := AuditEntry{
Username: req.Username,
Action: "login",
Success: true,
}
auditJSON, _ := json.Marshal(audit)
fmt.Printf("AUDIT: %s\n", auditJSON)
// Response
resp := LoginResponse{Token: "eyJ...", ExpiresIn: 3600}
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(resp)
fmt.Printf("RESPONSE: %s", buf.String())
}
func main() {
fmt.Println("=== Valid request ===")
processLogin(`{"username":"alice","password":"s3cr3tP@ss"}`)
fmt.Println("=== Parameter pollution ===")
processLogin(`{"username":"alice","password":"s3cr3tP@ss","admin":true}`)
fmt.Println("=== Missing password ===")
processLogin(`{"username":"alice"}`)
}
Real-World Context
DisallowUnknownFields() blocks parameter pollution — an attacker adding "admin": true to gain elevated access. The json:"-" tag prevents passwords from appearing in logs. This is OWASP A02 (Cryptographic Failures) — even a logging mistake can expose credentials.