encoding/json en Go avanzado: custom marshalers, omitempty, embedded structs y streaming

El paquete encoding/json de Go cubre los casos habituales con json.Marshal y json.Unmarshal, pero los servicios reales necesitan más: campos opcionales, payloads dinámicos, tipos que el encoder no conoce por defecto y procesado de streams sin cargar todo en memoria. Esta guía cubre las funcionalidades avanzadas con ejemplos de la API de GitHub.

Tags json: omitempty y json:-

Los struct tags controlan cómo se serializa cada campo. omitempty omite el campo si está vacío. json:"-" lo excluye siempre:

type Repositorio struct {
    ID          int       `json:"id"`
    NombreCompleto string `json:"full_name"`
    Privado     bool      `json:"private"`
    Descripcion string    `json:"description,omitempty"` // omitido si ""
    Token       string    `json:"-"`                      // nunca serializado
    CreadoEn    time.Time `json:"created_at"`
}

Embedded structs para reutilizar campos

Los structs embebidos funcionan con JSON: sus campos aparecen al mismo nivel que los del struct exterior:

type Timestamps struct {
    CreadoEn     time.Time `json:"created_at"`
    ActualizadoEn time.Time `json:"updated_at"`
}

type Issue struct {
    ID     int    `json:"id"`
    Titulo string `json:"title"`
    Estado string `json:"state"`
    Timestamps       // campos de Timestamps se aplanan en el JSON
}

// JSON resultante: {"id":1,"title":"bug","state":"open","created_at":"...","updated_at":"..."}

Custom Marshaler y Unmarshaler

Implementa json.Marshaler y json.Unmarshaler para controlar exactamente cómo se serializa un tipo:

type Moneda int64 // centavos

func (m Moneda) MarshalJSON() ([]byte, error) {
    // Serializar como "12.34" en lugar de 1234
    return json.Marshal(fmt.Sprintf("%.2f", float64(m)/100))
}

func (m *Moneda) UnmarshalJSON(data []byte) error {
    var s string
    if err := json.Unmarshal(data, &s); err != nil {
        return err
    }
    var f float64
    if _, err := fmt.Sscanf(s, "%f", &f); err != nil {
        return err
    }
    *m = Moneda(f * 100)
    return nil
}

type Pedido struct {
    ID    int     `json:"id"`
    Total Moneda  `json:"total"` // serializa como "15.99"
}

json.RawMessage para payloads dinámicos

json.RawMessage guarda el JSON crudo sin deserializar, lo que permite decidir el tipo más tarde según el contexto:

type Evento struct {
    Tipo   string          `json:"type"`
    Payload json.RawMessage `json:"payload"`
}

func procesarEvento(data []byte) error {
    var evento Evento
    if err := json.Unmarshal(data, &evento); err != nil {
        return err
    }

    switch evento.Tipo {
    case "push":
        var push PushEvent
        return json.Unmarshal(evento.Payload, &push)
    case "pull_request":
        var pr PREvent
        return json.Unmarshal(evento.Payload, &pr)
    default:
        return fmt.Errorf("tipo desconocido: %s", evento.Tipo)
    }
}

json.Number para enteros grandes

Por defecto, JSON decodifica números en float64, lo que pierde precisión con enteros de 64 bits. json.Number conserva la representación textual:

func decodificarConNumeros(data []byte) map[string]any {
    var resultado map[string]any
    dec := json.NewDecoder(bytes.NewReader(data))
    dec.UseNumber() // los números llegan como json.Number, no float64
    dec.Decode(&resultado)

    if id, ok := resultado["id"].(json.Number); ok {
        n, _ := id.Int64() // sin pérdida de precisión
        fmt.Println("ID:", n)
    }
    return resultado
}

Streaming con json.Encoder y json.Decoder

Para ficheros grandes o respuestas HTTP con arrays, procesa el JSON elemento a elemento sin cargar todo en memoria:

func procesarListadoGrande(r io.Reader) error {
    dec := json.NewDecoder(r)

    // Leer el '[' de apertura del array
    if _, err := dec.Token(); err != nil {
        return err
    }

    for dec.More() {
        var repo Repositorio
        if err := dec.Decode(&repo); err != nil {
            return err
        }
        procesarRepositorio(repo)
    }

    // Leer el ']' de cierre
    _, err := dec.Token()
    return err
}

func escribirNDJSON(w io.Writer, repos []Repositorio) error {
    enc := json.NewEncoder(w)
    for _, r := range repos {
        if err := enc.Encode(r); err != nil { // escribe línea por línea
            return err
        }
    }
    return nil
}

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP