json-go-marshaling-unmarshaling-struct-tags

JSON en Go: marshaling, unmarshaling y struct tags

  • 5 min

El paquete encoding/json permite convertir valores de Go en JSON y JSON en valores de Go. Es una de esas herramientas que vais a usar constantemente en APIs.

JSON aparece constantemente en APIs, archivos de configuración y almacenamiento de documentos. Por eso vamos a convertir datos de Go a JSON y viceversa.

A diferencia de JavaScript, donde JSON es casi nativo, en Go (al ser estáticamente tipado) necesitamos un proceso de traducción. A este proceso lo llamamos Marshaling (empaquetar a JSON) y Unmarshaling (desempaquetar a Go).

Vamos a usar el paquete estándar estable encoding/json y sus etiquetas de campos. Go 1.26 también incluye encoding/json/v2, pero sigue siendo experimental y no está cubierto por la promesa de compatibilidad de Go 1.

Marshaling: de Go a JSON

El proceso de convertir una estructura de datos de Go (como un Struct o un Map) en una secuencia de bytes JSON se llama Marshaling.

La función clave es json.Marshal.

package main

import (
    "encoding/json"
    "fmt"
)

type Usuario struct {
    Nombre string
    Edad   int
    Admin  bool
}

func main() {
    u := Usuario{"Luis", 30, true}

    // Convertimos el struct a JSON (devuelve []byte y error)
    jsonData, err := json.Marshal(u)
    if err != nil {
        panic(err)
    }

    // Convertimos los bytes a string para imprimirlo
    fmt.Println(string(jsonData))
}
Copied!

Salida: {"Nombre":"Luis","Edad":30,"Admin":true}

Solo se procesan los campos exportados

Fíjate en la salida anterior. Las claves del JSON son "Nombre", "Edad". ¿Qué pasaría si en el struct hubiéramos puesto nombre (en minúscula)?

El campo desaparecería del JSON.

El paquete encoding/json solo serializa los campos exportados, es decir, los que empiezan por mayúscula. Los campos no exportados se ignoran.

Etiquetas de campos

Generalmente, en las APIs no queremos devolver "Nombre", sino "nombre" (camelCase) o "first_name" (snake_case).

Como no podemos cambiar el campo del struct a minúscula, añadimos metadatos mediante struct tags.

Son cadenas escritas entre comillas invertidas a la derecha del tipo.

type Producto struct {
    ID          int     `json:"id"`            // Renombrar a "id"
    Nombre      string  `json:"product_name"`  // Renombrar a "product_name"
    Precio      float64 `json:"price,omitzero"`  // Renombrar y omitir si vale cero
    Password    string  `json:"-"`             // IGNORAR completamente
}
Copied!

Opciones comunes en las tags:

  1. json:"nombre": Cambia la clave en el JSON.
  2. omitempty: en encoding/json v1 omite false, 0, punteros e interfaces nil, y strings, arrays, slices o mapas de longitud cero. Un struct con valor cero no se considera vacío.
  3. omitzero: omite el valor cero de Go y respeta un método IsZero() bool si el tipo lo define. Es útil para números, booleanos y structs como time.Time.
  4. -: el campo se ignora siempre (útil para datos internos o sensibles).

Ejemplo con Tags

func main() {
    p := Producto{
        ID:       100,
        Nombre:   "Laptop",
        Precio:   0,        // Zero value
        Password: "123",    // Se ignorará
    }

    bytes, err := json.Marshal(p)
    if err != nil {
        return
    }
    fmt.Println(string(bytes))
}
Copied!

Salida: {"id":100,"product_name":"Laptop"}

Fíjate que:

  • id y product_name están en minúsculas.
  • price ha desaparecido porque era 0 y tenía omitzero.
  • Password ha desaparecido (porque tenía -).

Unmarshaling: de JSON a Go

La operación inversa es Unmarshaling. Consiste en coger una cadena JSON y rellenar un Struct existente.

La función es json.Unmarshal(data []byte, v any).

Importante: Debes pasar un PUNTERO al struct destino. Si lo pasas por valor, Unmarshal modificará una copia y tu struct original seguirá vacío.

func main() {
    jsonInput := `{"id": 50, "product_name": "Ratón", "extra": "dato ignorado"}`

    var p Producto

    // Pasamos &p (Puntero) y convertimos el string a []byte
    err := json.Unmarshal([]byte(jsonInput), &p)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    fmt.Printf("Producto decodificado: %+v\n", p)
}
Copied!

Salida: Producto decodificado: {ID:50 Nombre:Ratón Precio:0 Password:}

Al decodificar:

  • Mapea "product_name" al campo Nombre gracias al Tag.
  • Ignora por defecto las claves que no corresponden con el struct (como "extra").

Si queremos rechazar datos desconocidos, usamos un json.Decoder y llamamos a DisallowUnknownFields() antes de Decode.

JSON Genérico (map[string]any)

¿Qué pasa si recibes un JSON y no sabes qué estructura tiene? ¿O es un JSON dinámico donde los campos cambian?

En ese caso, puedes hacer Unmarshal sobre un mapa o una interfaz vacía.

jsonInput := `{"nombre": "Desconocido", "atributos": {"fuerza": 10}}`

var resultado map[string]any
if err := json.Unmarshal([]byte(jsonInput), &resultado); err != nil {
    return
}

fmt.Println(resultado["nombre"])

// Para acceder a "fuerza", tendremos que hacer Type Assertion
atributos, ok := resultado["atributos"].(map[string]any)
if ok {
    fmt.Println(atributos["fuerza"])
}
Copied!

Aunque flexible, obliga a comprobar aserciones de tipo. Además, los números se decodifican como float64 por defecto; un Decoder con UseNumber() permite conservarlos como json.Number. Siempre que podamos, es preferible definir un struct.

Encoder y Decoder frente a Marshal y Unmarshal

  • Marshal/Unmarshal: Trabajan con []byte. Requieren tener todo el JSON en memoria.
  • Encoder/Decoder: Trabajan con io.Writer/io.Reader y encajan bien con archivos, conexiones y cuerpos HTTP.

En un handler web, Encoder permite escribir sobre el ResponseWriter. Marshal sigue siendo apropiado si necesitamos los bytes antes de enviar, calcular una firma o asegurarnos de que la codificación funciona antes de escribir las cabeceras. Además, Encoder.Encode añade un salto de línea al final.

// Ejemplo en un handler web
func Handler(w http.ResponseWriter, r *http.Request) {
    p := Producto{ID: 1, Nombre: "Stream"}

    w.Header().Set("Content-Type", "application/json")
    if err := json.NewEncoder(w).Encode(p); err != nil {
        log.Printf("codificar respuesta JSON: %v", err)
    }
}
Copied!