Encoding Json #
JSON is the most common data exchange format in modern APIs — there’s almost no Go application that interacts with the outside world without touching JSON. The encoding/json package provides everything needed: converting Go structs to JSON (Marshal), loading JSON into structs (Unmarshal), streaming JSON to and from io.Reader/io.Writer, and mechanisms for customizing encoding and decoding behavior. What makes encoding/json interesting is its reflection-based approach — you define a struct with json: tags and the package handles all the conversion automatically. But there are many important details to understand: handling nil vs empty values, omitted fields, unmarshalable types, subtle decoding errors, and when streaming is better than buffered marshaling.
An Overview of the encoding/json Package #
flowchart LR
subgraph Encode["Struct → JSON"]
E1["json.Marshal(v)\nstruct → []byte"]
E2["json.MarshalIndent(v, '', ' ')\nstruct → []byte (pretty)"]
E3["json.NewEncoder(w).Encode(v)\nstruct → io.Writer (streaming)"]
end
subgraph Decode["JSON → Struct"]
D1["json.Unmarshal(data, &v)\n[]byte → struct"]
D2["json.NewDecoder(r).Decode(&v)\nio.Reader → struct (streaming)"]
end
subgraph Tags["Struct Tags"]
T1["json:"field_name""]
T2["json:"name,omitempty""]
T3["json:"-""]
T4["json:",string""]
end
subgraph Custom["Customization"]
C1["json.Marshaler interface\nMarshalJSON() ([]byte, error)"]
C2["json.Unmarshaler interface\nUnmarshalJSON([]byte) error"]
C3["json.RawMessage\nraw JSON without parsing"]
end
Encode --> JSON["JSON"]
JSON --> Decode
Tags --> Encode
Tags --> Decode
Custom --> Encode
Custom --> Decode
style Encode fill:#e8f5e9
style Decode fill:#e3f2fd
style Tags fill:#fff3e0
style Custom fill:#f3e5f5Marshal — Struct to JSON #
json.Marshal converts a Go value into a []byte containing JSON. It uses reflection to read struct fields and the json: tags to determine the field names in the output.
package main
import (
"encoding/json"
"fmt"
)
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
InStock bool `json:"in_stock"`
}
func main() {
p := Product{
ID: 1,
Name: "Laptop Go Edition",
Price: 15000000,
InStock: true,
}
// Marshal into a []byte
data, err := json.Marshal(p)
if err != nil {
fmt.Println("marshal error:", err)
return
}
fmt.Println(string(data))
// {"id":1,"name":"Laptop Go Edition","price":1.5e+07,"in_stock":true}
// MarshalIndent — for human-readable output
dataIndent, err := json.MarshalIndent(p, "", " ")
if err != nil {
fmt.Println("marshal error:", err)
return
}
fmt.Println(string(dataIndent))
// {
// "id": 1,
// "name": "Laptop Go Edition",
// "price": 1.5e+07,
// "in_stock": true
// }
}
What Can and Can’t Be Marshaled #
flowchart TD
V["Go value"] --> Can{"Can it\nbe marshaled?"}
Can -- Yes --> Y1["struct — becomes a JSON object"]
Can -- Yes --> Y2["map[string]T — becomes a JSON object"]
Can -- Yes --> Y3["[]T, [N]T — becomes a JSON array"]
Can -- Yes --> Y4["string — becomes a JSON string"]
Can -- Yes --> Y5["int, float — becomes a JSON number"]
Can -- Yes --> Y6["bool — becomes true/false"]
Can -- Yes --> Y7["nil pointer — becomes null"]
Can -- Yes --> Y8["time.Time — becomes an RFC3339 string"]
Can -- Error --> N1["channel — can't"]
Can -- Error --> N2["func — can't"]
Can -- Error --> N3["complex — can't"]
Can -- Error --> N4["maps with non-string\nnon-int keys"]
style Y1 fill:#e8f5e9
style Y2 fill:#e8f5e9
style Y3 fill:#e8f5e9
style Y4 fill:#e8f5e9
style Y5 fill:#e8f5e9
style Y6 fill:#e8f5e9
style Y7 fill:#e8f5e9
style Y8 fill:#e8f5e9
style N1 fill:#fce4ec
style N2 fill:#fce4ec
style N3 fill:#fce4ec
style N4 fill:#fff3e0// Marshalable types
m := map[string]int{"a": 1, "b": 2}
data, _ := json.Marshal(m)
fmt.Println(string(data)) // {"a":1,"b":2}
slice := []string{"one", "two", "three"}
data, _ = json.Marshal(slice)
fmt.Println(string(data)) // ["one","two","three"]
// nil — becomes null
var p *Product = nil
data, _ = json.Marshal(p)
fmt.Println(string(data)) // null
// interface{} / any — marshal the value inside
var v any = map[string]any{
"name": "Budi",
"age": 30,
"active": true,
}
data, _ = json.Marshal(v)
fmt.Println(string(data)) // {"active":true,"age":30,"name":"Budi"}
// Types that CAN'T be marshaled
ch := make(chan int)
_, err := json.Marshal(ch)
fmt.Println(err) // json: unsupported type: chan int
Struct Tags — Controlling Serialization #
The json: struct tag is the main mechanism for controlling how struct fields are represented in JSON.
type User struct {
// Different field names between Go and JSON
ID int `json:"id"`
FullName string `json:"full_name"`
// omitempty — omit the field if it's the zero value
Phone string `json:"phone,omitempty"` // omitted if ""
Age int `json:"age,omitempty"` // omitted if 0
Active bool `json:"active,omitempty"` // omitted if false
Score float64 `json:"score,omitempty"` // omitted if 0.0
Tags []string `json:"tags,omitempty"` // omitted if nil or []
// - (minus) — always omit this field from JSON
Password string `json:"-"`
Token string `json:"-"`
// ,string — encode/decode numbers as JSON strings
// useful for JavaScript, which can't handle int64 precisely
ExternalID int64 `json:"external_id,string"`
// No tag — the Go field name is used as-is
Note string // → "Note" in JSON
// Embedded struct — its fields "rise" to the top level
Address
}
type Address struct {
City string `json:"city"`
Province string `json:"province"`
}
// Example output
p := User{
ID: 1,
FullName: "Budi Santoso",
Phone: "", // omitempty — doesn't appear
Age: 0, // omitempty — doesn't appear
ExternalID: 12345678901234,
Password: "secret", // json:"-" — doesn't appear
Address: Address{
City: "Jakarta",
Province: "DKI Jakarta",
},
}
data, _ := json.MarshalIndent(p, "", " ")
fmt.Println(string(data))
// {
// "id": 1,
// "full_name": "Budi Santoso",
// "external_id": "12345678901234", ← number as a string
// "Note": "",
// "city": "Jakarta", ← from the embedded Address
// "province": "DKI Jakarta"
// }
Pointers to Distinguish Zero Value from Absent #
omitempty can’t distinguish between false deliberately set and false because it wasn’t filled in. Pointers solve this problem:
// ANTI-PATTERN: can't distinguish "active=false" vs "not set"
type BadConfig struct {
Active bool `json:"active,omitempty"`
// If Active=false, this field disappears from the JSON
// even though false could mean "deliberately disabled"!
}
// CORRECT: use a pointer for values that can be intentionally null
type GoodConfig struct {
Active *bool `json:"active,omitempty"`
// nil → omitted from the JSON (not set)
// &false → appears as false (deliberately disabled)
// &true → appears as true (deliberately enabled)
}
// Helper for creating pointers to literals
func boolPtr(b bool) *bool { return &b }
func intPtr(n int) *int { return &n }
cfg := GoodConfig{
Active: boolPtr(false), // appears as "active": false
}
data, _ := json.Marshal(cfg)
fmt.Println(string(data)) // {"active":false}
cfg2 := GoodConfig{
Active: nil, // doesn't appear at all
}
data, _ = json.Marshal(cfg2)
fmt.Println(string(data)) // {}
Unmarshal — JSON to Struct #
json.Unmarshal loads JSON from a []byte into a struct. Fields missing from the JSON are left at their zero values, and JSON fields not in the struct are ignored.
type Article struct {
ID int `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
Tags []string `json:"tags"`
Published bool `json:"published"`
}
jsonData := []byte(`{
"id": 42,
"title": "Learning Go",
"content": "Go is a fun language",
"tags": ["go", "programming", "tutorial"],
"published": true,
"unknown_field": "ignored"
}`)
var article Article
err := json.Unmarshal(jsonData, &article)
if err != nil {
fmt.Println("unmarshal error:", err)
return
}
fmt.Println(article.ID) // 42
fmt.Println(article.Title) // Learning Go
fmt.Println(article.Tags) // [go programming tutorial]
fmt.Println(article.Published) // true
Unmarshaling into a map — Dynamic Structures #
When the JSON structure isn’t known in advance, unmarshal into a map[string]any:
jsonData := []byte(`{
"name": "Budi",
"age": 30,
"active": true,
"score": 98.5,
"tags": ["admin", "user"]
}`)
var result map[string]any
err := json.Unmarshal(jsonData, &result)
if err != nil {
fmt.Println("error:", err)
return
}
// Access values with type assertions
name := result["name"].(string)
age := result["age"].(float64) // NOTE: all JSON numbers → float64!
active := result["active"].(bool)
fmt.Printf("Name: %s, Age: %.0f, Active: %v\n", name, age, active)
// Iterate all fields
for key, val := range result {
fmt.Printf("%s (%T): %v\n", key, val, val)
}
When unmarshaling into amap[string]anyorinterface{}, all JSON numbers are converted tofloat64, notint. This is the default behavior of theencoding/jsonpackage. To get numbers asint, usejson.NumberwithDecoder.UseNumber(), or unmarshal into a struct with the right types.
Handling Unknown Fields #
// Detect unknown fields in JSON input
type Config struct {
Host string `json:"host"`
Port int `json:"port"`
}
jsonData := []byte(`{"host":"localhost","port":8080,"debug":true}`)
// DisallowUnknownFields — errors if there's an unknown field
decoder := json.NewDecoder(strings.NewReader(string(jsonData)))
decoder.DisallowUnknownFields()
var cfg Config
if err := decoder.Decode(&cfg); err != nil {
fmt.Println(err)
// json: unknown field "debug"
}
Encoder and Decoder — Streaming JSON #
json.NewEncoder and json.NewDecoder work directly with io.Writer and io.Reader — more efficient than Marshal/Unmarshal because they don’t need to buffer the entire data in memory.
flowchart LR
subgraph Buffered["Buffered (Marshal/Unmarshal)"]
B1["Struct"] --> B2["json.Marshal"] --> B3["[]byte\n(entire data in memory)"] --> B4["io.Writer"]
B5["io.Reader"] --> B6["read everything\ninto []byte"] --> B7["json.Unmarshal"] --> B8["Struct"]
end
subgraph Stream["Streaming (Encoder/Decoder)"]
S1["Struct"] --> S2["Encoder.Encode"] --> S3["io.Writer\n(write directly)"]
S4["io.Reader"] --> S5["Decoder.Decode"] --> S6["Struct\n(read per token)"]
end
subgraph When["Use Streaming If"]
W1["HTTP response — ResponseWriter is an io.Writer"]
W2["Large JSON files — don't fit in memory"]
W3["JSON Lines — many objects in one stream"]
W4["Request body — Body is an io.Reader"]
end
style Buffered fill:#fff3e0
style Stream fill:#e8f5e9
style When fill:#e3f2fdEncoder — Writing JSON to an io.Writer #
import (
"encoding/json"
"net/http"
"os"
)
// Writing JSON to an HTTP response (most common)
func productHandler(w http.ResponseWriter, r *http.Request) {
product := Product{ID: 1, Name: "Laptop", Price: 15000000}
w.Header().Set("Content-Type", "application/json")
// CORRECT: use an Encoder directly to the ResponseWriter
if err := json.NewEncoder(w).Encode(product); err != nil {
// If the error happens here, the header is already sent — can't change the status code
// Just log it
fmt.Fprintf(os.Stderr, "encode error: %v\n", err)
}
}
// Writing JSON to a file
func saveToFile(path string, data any) error {
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("saveToFile: %w", err)
}
defer f.Close()
encoder := json.NewEncoder(f)
encoder.SetIndent("", " ") // pretty print
if err := encoder.Encode(data); err != nil {
return fmt.Errorf("saveToFile encode: %w", err)
}
return nil
}
// Writing many objects as JSON Lines (NDJSON)
// Format: one JSON object per line, useful for logs and streams
func writeJSONLines(w io.Writer, items []Product) error {
encoder := json.NewEncoder(w)
for _, item := range items {
if err := encoder.Encode(item); err != nil {
return fmt.Errorf("writeJSONLines: %w", err)
}
// Encode automatically adds a newline after every object
}
return nil
}
Decoder — Reading JSON from an io.Reader #
// Reading JSON from an HTTP request body
func createProductHandler(w http.ResponseWriter, r *http.Request) {
var product Product
// CORRECT: use a Decoder directly from the Body
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields() // optional: reject unknown fields
if err := decoder.Decode(&product); err != nil {
http.Error(w, "invalid request body: "+err.Error(),
http.StatusBadRequest)
return
}
// Validate after decoding
if product.Name == "" {
http.Error(w, "product name must not be empty", http.StatusBadRequest)
return
}
// Process the product...
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(product)
}
// Reading JSON Lines from a file — one object per line
func readJSONLines(path string) ([]Product, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("readJSONLines: %w", err)
}
defer f.Close()
var products []Product
decoder := json.NewDecoder(f)
for decoder.More() { // More() returns true if there's more data
var p Product
if err := decoder.Decode(&p); err != nil {
return nil, fmt.Errorf("readJSONLines decode: %w", err)
}
products = append(products, p)
}
return products, nil
}
json.RawMessage — Raw JSON #
json.RawMessage is a []byte implementing json.Marshaler and json.Unmarshaler — useful for deferring the parsing of part of a JSON or passing JSON through unchanged.
// Pattern 1: a field whose content is free-form JSON (schema unknown)
type EventLog struct {
EventType string `json:"type"`
Time time.Time `json:"time"`
Data json.RawMessage `json:"data"` // any JSON
}
// Marshal: Data stays as JSON as-is
log := EventLog{
EventType: "purchase",
Time: time.Now(),
Data: json.RawMessage(`{"product_id":42,"quantity":2,"total":30000}`),
}
data, _ := json.Marshal(log)
fmt.Println(string(data))
// {"type":"purchase","time":"...","data":{"product_id":42,"quantity":2,"total":30000}}
// Unmarshal: Data isn't parsed, stored as-is
var logRead EventLog
json.Unmarshal(data, &logRead)
fmt.Println(string(logRead.Data))
// {"product_id":42,"quantity":2,"total":30000}
// Pattern 2: a discriminated union — parse based on the type field
type Message struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload"`
}
type ChatPayload struct {
Text string `json:"text"`
Sender string `json:"sender"`
}
type ImagePayload struct {
URL string `json:"url"`
Size int `json:"size"`
}
func parseMessage(data []byte) (any, error) {
var msg Message
if err := json.Unmarshal(data, &msg); err != nil {
return nil, err
}
switch msg.Type {
case "chat":
var p ChatPayload
if err := json.Unmarshal(msg.Payload, &p); err != nil {
return nil, err
}
return p, nil
case "image":
var p ImagePayload
if err := json.Unmarshal(msg.Payload, &p); err != nil {
return nil, err
}
return p, nil
default:
return nil, fmt.Errorf("unknown message type: %s", msg.Type)
}
}
Custom Marshalers and Unmarshalers #
To fully control how a type is converted to and from JSON, implement the json.Marshaler and json.Unmarshaler interfaces.
sequenceDiagram
participant App as Application code
participant Enc as json.Marshal
participant Type as Type with MarshalJSON
App->>Enc: json.Marshal(value)
Enc->>Enc: Check whether the type\nimplements json.Marshaler
Enc->>Type: MarshalJSON()
Type-->>Enc: custom JSON []byte
Enc-->>App: final JSON
Note over Enc,Type: Same for Unmarshal → UnmarshalJSON([]byte)// Example 1: time.Time with a custom format (not the default RFC3339)
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
}
parsed, err := time.Parse("2006-01-02", s)
if err != nil {
return fmt.Errorf("Date.UnmarshalJSON: format must be YYYY-MM-DD: %w", err)
}
d.Time = parsed
return nil
}
type Order struct {
ID int `json:"id"`
CreatedOn Date `json:"created_on"`
Total float64 `json:"total"`
}
// Output: {"id":1,"created_on":"2024-03-15","total":150000}
// not: {"id":1,"created_on":"2024-03-15T00:00:00Z","total":150000}
// Example 2: enums / constants as strings
type OrderStatus int
const (
StatusPending OrderStatus = iota
StatusProcessing
StatusShipped
StatusCompleted
StatusCancelled
)
var statusNames = map[OrderStatus]string{
StatusPending: "pending",
StatusProcessing: "processing",
StatusShipped: "shipped",
StatusCompleted: "completed",
StatusCancelled: "cancelled",
}
var nameStatuses = map[string]OrderStatus{
"pending": StatusPending,
"processing": StatusProcessing,
"shipped": StatusShipped,
"completed": StatusCompleted,
"cancelled": StatusCancelled,
}
func (s OrderStatus) MarshalJSON() ([]byte, error) {
name, ok := statusNames[s]
if !ok {
return nil, fmt.Errorf("OrderStatus.MarshalJSON: invalid value: %d", s)
}
return json.Marshal(name)
}
func (s *OrderStatus) UnmarshalJSON(data []byte) error {
var name string
if err := json.Unmarshal(data, &name); err != nil {
return err
}
status, ok := nameStatuses[name]
if !ok {
return fmt.Errorf("OrderStatus.UnmarshalJSON: invalid value: %q", name)
}
*s = status
return nil
}
type Order2 struct {
ID int `json:"id"`
Status OrderStatus `json:"status"`
}
p := Order2{ID: 1, Status: StatusShipped}
data, _ := json.Marshal(p)
fmt.Println(string(data)) // {"id":1,"status":"shipped"}
Handling JSON Errors Correctly #
Errors from json.Unmarshal can be of various types — understanding them helps provide more informative error messages to users.
import (
"encoding/json"
"errors"
)
func decodeRequest(body io.Reader, target any) error {
decoder := json.NewDecoder(body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(target); err != nil {
var syntaxErr *json.SyntaxError
var typeErr *json.UnmarshalTypeError
switch {
case errors.As(err, &syntaxErr):
return fmt.Errorf("invalid JSON at position %d: %w",
syntaxErr.Offset, err)
case errors.As(err, &typeErr):
return fmt.Errorf("type mismatch: field %q expected %v, got %v",
typeErr.Field, typeErr.Type, typeErr.Value)
case errors.Is(err, io.EOF):
return fmt.Errorf("request body is empty")
case errors.Is(err, io.ErrUnexpectedEOF):
return fmt.Errorf("incomplete JSON")
default:
return fmt.Errorf("failed to decode request: %w", err)
}
}
return nil
}
// Usage in a handler
func createUserHandler(w http.ResponseWriter, r *http.Request) {
var user User
if err := decodeRequest(r.Body, &user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// continue...
}
Production Usage Patterns #
Consistent API Responses #
// A uniform API response structure
type APIResponse struct {
Success bool `json:"success"`
Data any `json:"data,omitempty"`
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
}
func sendJSON(w http.ResponseWriter, statusCode int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
if err := json.NewEncoder(w).Encode(data); err != nil {
fmt.Fprintf(os.Stderr, "sendJSON encode error: %v\n", err)
}
}
func sendSuccess(w http.ResponseWriter, data any) {
sendJSON(w, http.StatusOK, APIResponse{
Success: true,
Data: data,
})
}
func sendError(w http.ResponseWriter, statusCode int, message string) {
sendJSON(w, statusCode, APIResponse{
Success: false,
Error: message,
})
}
// Usage in a handler
func listProductsHandler(w http.ResponseWriter, r *http.Request) {
products, err := listProductsService()
if err != nil {
sendError(w, http.StatusInternalServerError, "failed to load products")
return
}
sendSuccess(w, products)
}
Configuration from a JSON File #
type AppConfig struct {
Server ServerConfig `json:"server"`
Database DatabaseConfig `json:"database"`
Log LogConfig `json:"log"`
}
type ServerConfig struct {
Host string `json:"host"`
Port int `json:"port"`
ReadTimeout time.Duration `json:"read_timeout"`
WriteTimeout time.Duration `json:"write_timeout"`
}
type DatabaseConfig struct {
DSN string `json:"dsn"`
MaxOpenConn int `json:"max_open_conn"`
MaxIdleConn int `json:"max_idle_conn"`
}
type LogConfig struct {
Level string `json:"level"`
Format string `json:"format"`
}
func loadConfig(path string) (*AppConfig, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("loadConfig: %w", err)
}
defer f.Close()
// Default values before decoding
cfg := &AppConfig{
Server: ServerConfig{
Host: "0.0.0.0",
Port: 8080,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
},
Log: LogConfig{
Level: "info",
Format: "json",
},
}
decoder := json.NewDecoder(f)
decoder.DisallowUnknownFields()
if err := decoder.Decode(cfg); err != nil {
return nil, fmt.Errorf("loadConfig decode: %w", err)
}
return cfg, nil
}
Transforming JSON Without Structs #
// Rename a JSON field without a full struct
func renameJSONField(input []byte, old, new string) ([]byte, error) {
var data map[string]json.RawMessage
if err := json.Unmarshal(input, &data); err != nil {
return nil, err
}
if val, exists := data[old]; exists {
data[new] = val
delete(data, old)
}
return json.Marshal(data)
}
// Filter sensitive fields before sending to the client
type PublicUser struct {
ID int `json:"id"`
Name string `json:"name"`
// Password, Token, etc. are not here
}
func filterUser(u *User) PublicUser {
return PublicUser{ID: u.ID, Name: u.FullName}
}
Streaming Many Objects #
// Export thousands of products to JSON Lines without buffering everything in memory
func exportProducts(w io.Writer, repo ProductRepository) error {
encoder := json.NewEncoder(w)
// Iterate from the database with a cursor, not loading everything at once
cursor, err := repo.Cursor()
if err != nil {
return fmt.Errorf("exportProducts: open cursor: %w", err)
}
defer cursor.Close()
count := 0
for cursor.Next() {
product, err := cursor.Scan()
if err != nil {
return fmt.Errorf("exportProducts: scan row %d: %w", count, err)
}
if err := encoder.Encode(product); err != nil {
return fmt.Errorf("exportProducts: encode row %d: %w", count, err)
}
count++
}
fmt.Fprintf(os.Stderr, "Export done: %d products\n", count)
return cursor.Err()
}
When to Switch to Alternatives #
Keep using encoding/json if:
✓ Marshaling and unmarshaling structs to/from JSON
✓ Streaming JSON to HTTP responses or from request bodies
✓ Simple configuration from JSON files
✓ Internal APIs between Go services
Consider encoding/json with json.Number if:
✗ Unmarshaling large numbers that can't be represented by float64
✗ You need to know the original number type (int vs float) from unstructured JSON
Consider external libraries if:
✗ Performance is very critical (thousands of marshals per second) → sonic, go-json, jsoniter
(can be 3-10x faster than encoding/json for certain cases)
✗ JSON Schema validation → gojsonschema or a validator library
✗ JSONPath queries → gjson for field access without full unmarshaling
✗ JSON Patch / JSON Merge Patch → RFC 6902 implementations
✗ YAML with JSON conversion → gopkg.in/yaml.v3
Summary #
- Struct tags
json:"name"control the field names in JSON — use snake_case for compatibility with common JSON conventions.omitemptyomits the field if its value is the zero value ("",0,false,nil) — use pointers (*bool,*int) if you need to distinguish “not set” from “set to zero”.json:"-"always omits the field from JSON — use it for Password, Token, and other sensitive fields.- All JSON numbers become
float64when unmarshaling intointerface{}ormap[string]any— usedecoder.UseNumber()or unmarshal into a struct with the right types if precision matters.- Use
json.NewEncoder(w).Encode(v)instead ofjson.Marshalfor HTTP responses — more efficient because it doesn’t buffer all the data in memory.- Use
json.NewDecoder(r.Body).Decode(&v)instead ofjson.Unmarshalfor request bodies — streams directly from the body without reading everything into a[]bytefirst.json.RawMessagelets you store and pass through raw JSON without parsing — useful for discriminated unions or fields with dynamic schemas.- Custom
MarshalJSON/UnmarshalJSONgive full control over the format — use them for enums, custom date formats, or representations that can’t be expressed with tags alone.decoder.DisallowUnknownFields()helps detect typos or unsupported fields in request bodies — useful for strict APIs.errors.Aswith*json.SyntaxErrorand*json.UnmarshalTypeErrorenables specific error messages about what’s wrong in the JSON input.