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))
}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
}Opciones comunes en las tags:
json:"nombre": Cambia la clave en el JSON.omitempty: enencoding/jsonv1 omitefalse,0, punteros e interfacesnil, y strings, arrays, slices o mapas de longitud cero. Un struct con valor cero no se considera vacío.omitzero: omite el valor cero de Go y respeta un métodoIsZero() boolsi el tipo lo define. Es útil para números, booleanos y structs comotime.Time.-: 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))
}Salida:
{"id":100,"product_name":"Laptop"}
Fíjate que:
idyproduct_nameestán en minúsculas.priceha desaparecido porque era 0 y teníaomitzero.Passwordha 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)
}Salida:
Producto decodificado: {ID:50 Nombre:Ratón Precio:0 Password:}
Al decodificar:
- Mapea
"product_name"al campoNombregracias 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"])
}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 conio.Writer/io.Readery 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)
}
}