Convertir un struct a JSON en Go

encoding/json convierte un struct en JSON con una llamada. Lo que decide el resultado son las etiquetas de cada campo y una regla de Go que sorprende la primera vez: los campos en minúscula no salen.

Sin etiquetas

type SinEtiquetas struct {
	Nombre string
	correo string
}

b, _ := json.Marshal(SinEtiquetas{"Ana", "ana@ejemplo.com"})
fmt.Println(string(b))
salida{"Nombre":"Ana"}

Dos cosas. La clave sale con mayúscula, como el campo. Y el correo ha desaparecido: en Go, un nombre que empieza por minúscula es privado del paquete, y encoding/json, que está en otro paquete, no puede verlo. No da error; simplemente lo ignora.

Con etiquetas

type Usuario struct {
	Nombre   string    `json:"nombre"`
	Correo   string    `json:"correo"`
	Password string    `json:"-"`
	Edad     int       `json:"edad,omitempty"`
	Alta     time.Time `json:"alta,omitzero"`
}
con Password, sin Edad ni Alta{"nombre":"Ana","correo":"ana@ejemplo.com"}
EtiquetaQué hace
json:"nombre"el nombre de la clave en el JSON
json:"-"no sale nunca
omitemptyno sale si vale cero, cadena vacía, nil o colección vacía
omitzerono sale si es el valor cero de su tipo; desde Go 1.24
La contraseña no debe salir nunca en un JSON. La versión anterior de esta página la serializaba con json:"password" y aparecía en el resultado. Basta con que algún día alguien devuelva ese struct en una respuesta de una API para que las contraseñas viajen a quien las pida. json:"-" lo impide de raíz.

¿Por qué existe omitzero si ya había omitempty? Porque una fecha vacía, un time.Time, es un struct, y para omitempty un struct nunca está «vacío»: la fecha salía como "0001-01-01T00:00:00Z". omitzero lo resuelve.

Con sangría

b, _ := json.MarshalIndent(u, "", "  ")
salida{
  "nombre": "Ana",
  "correo": "ana@ejemplo.com",
  "edad": 30,
  "alta": "2026-10-08T09:30:00Z"
}

De JSON a struct

var leido Usuario
err := json.Unmarshal([]byte(`{"nombre":"Luis","edad":"treinta"}`), &leido)
fmt.Println("error:", err)
salidaerror: json: cannot unmarshal string into Go struct field Usuario.edad of type int

Hay que pasar un puntero, &leido, para que pueda rellenarlo. Si un tipo no encaja, el error dice exactamente qué campo y qué esperaba. Lo contrario, en cambio, no da error: las claves que no existen en el struct se ignoran sin avisar, así que una errata en el nombre de un campo hace que llegue vacío y no te enteras. Si quieres que falle, usa un json.Decoder con DisallowUnknownFields().

Sin struct

var cualquiera map[string]any
json.Unmarshal([]byte(`{"n": 42, "lista": [1, 2]}`), &cualquiera)
fmt.Printf("%T %v | %T\n", cualquiera["n"], cualquiera["n"], cualquiera["lista"])
salidafloat64 42 | []interface {}

Cuando no sabes la forma del JSON. Cuidado: todos los números llegan como float64, también los enteros, y para usarlos hay que convertirlos.

Más información en inglés: Convert a struct to JSON in Golang.

Todo el código se compiló y se ejecutó con Go 1.27 en un contenedor limpio antes de publicar esta página; las salidas están copiadas de esa ejecución.