JSON en Go: Marshal, Unmarshal y struct tags para mapear campos

El paquete encoding/json de Go convierte entre valores Go y JSON de forma bidireccional. json.Marshal serializa un valor Go a JSON y json.Unmarshal deserializa JSON a un valor Go. Los struct tags controlan cómo se mapean los campos, qué claves se usan en el JSON y cuándo se omiten valores.

Marshal: de Go a JSON

package main

import (
    "encoding/json"
    "fmt"
)

type Persona struct {
    Nombre string `json:"nombre"`
    Edad   int    `json:"edad"`
    Email  string `json:"email"`
}

func main() {
    p := Persona{Nombre: "Ana", Edad: 30, Email: "[email protected]"}

    datos, err := json.Marshal(p)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(datos))
    // {"nombre":"Ana","edad":30,"email":"[email protected]"}

    // con indentado para debug
    bonito, _ := json.MarshalIndent(p, "", "  ")
    fmt.Println(string(bonito))
}

Unmarshal: de JSON a Go

jsonStr := `{"nombre":"Luis","edad":25,"email":"[email protected]"}`

var p Persona
if err := json.Unmarshal([]byte(jsonStr), &p); err != nil {
    panic(err)
}
fmt.Println(p.Nombre) // Luis
fmt.Println(p.Edad)   // 25

Struct tags: opciones importantes

type Producto struct {
    ID          int     `json:"id"`
    Nombre      string  `json:"nombre"`
    Precio      float64 `json:"precio,omitempty"` // omite si es 0
    Descripcion string  `json:"descripcion,omitempty"` // omite si es ""
    Interno     string  `json:"-"` // nunca se serializa
    SinTag      string  // usa el nombre del campo: "SinTag"
}

Las opciones más comunes en el tag son:

  • json:"nombre" — renombra el campo en el JSON
  • json:"nombre,omitempty" — omite el campo si tiene su zero value
  • json:"-" — excluye el campo siempre

json.Decoder para peticiones HTTP

Para leer JSON de un flujo (body de una petición HTTP, fichero...) usa json.NewDecoder en vez de leer todo en memoria con Unmarshal:

func handler(w http.ResponseWriter, r *http.Request) {
    var p Persona
    decoder := json.NewDecoder(r.Body)
    decoder.DisallowUnknownFields() // rechaza claves desconocidas

    if err := decoder.Decode(&p); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    // usar p...
}

Tipos anidados

type Direccion struct {
    Calle   string `json:"calle"`
    Ciudad  string `json:"ciudad"`
    CodigoP string `json:"codigo_postal"`
}

type Cliente struct {
    Nombre    string    `json:"nombre"`
    Direccion Direccion `json:"direccion"` // struct anidado
    Tags      []string  `json:"tags"`      // array JSON
}

Tipos personalizados con MarshalJSON

Implementa MarshalJSON y UnmarshalJSON para controlar exactamente cómo se representa un tipo:

type Precio struct {
    Centimos int
}

func (p Precio) MarshalJSON() ([]byte, error) {
    return json.Marshal(float64(p.Centimos) / 100)
}

func (p *Precio) UnmarshalJSON(data []byte) error {
    var f float64
    if err := json.Unmarshal(data, &f); err != nil {
        return err
    }
    p.Centimos = int(f * 100)
    return nil
}

// JSON: 9.99 ? Go: Precio{Centimos: 999}

map string interface para claves desconocidas

Cuando no conoces la estructura del JSON de antemano:

jsonStr := `{"nombre":"Ana","activo":true,"puntuacion":9.5}`

var datos map[string]any
json.Unmarshal([]byte(jsonStr), &datos)

fmt.Println(datos["nombre"])     // Ana
fmt.Println(datos["activo"])     // true
fmt.Println(datos["puntuacion"]) // 9.5

Recuerda que los números en JSON llegan como float64 cuando el destino es any, no como int. Si necesitas enteros, convierte explícitamente o usa json.Number.

COMPARTE ESTE ARTÍCULO

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