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
}
