Marshal y Unmarshal
json.Marshal convierte un valor Go en bytes JSON. json.Unmarshal rellena un valor Go a partir de bytes JSON. Los struct tags eligen los nombres de los campos JSON.
Email falta en la primera salida por omitempty y un string vacío. Unmarshal necesita un puntero (&back); pasar el valor directamente devuelve un InvalidUnmarshalError.
Struct tags
La sintaxis del tag es json:"name,option,option".
| Tag | Efecto |
|---|---|
json:"user_id" | usa user_id como clave |
json:"email,omitempty" | omite si es false, 0, "", nil, o un slice o map vacíos |
json:",omitzero" (Go 1.24) | omite si el valor es su valor cero, o si su IsZero() devuelve true |
json:"-" | nunca codifica ni decodifica este campo |
json:"-," | usa la clave literal - |
json:"count,string" | codifica un número o un bool como string JSON ("42") |
| sin tag | la clave es el nombre del campo en Go, UserID |
La contraseña nunca sale del struct, el saldo va entre comillas y deleted_at desaparece mientras que created_at imprime el tiempo cero 0001-01-01T00:00:00Z. Esa diferencia es la razón de que se añadiera omitzero: omitempty nunca ha funcionado con structs, y con time.Time es una sorpresa de toda la vida. En Go 1.23 y anteriores, usa un *time.Time con omitempty para conseguir el mismo efecto.
MarshalIndent(v, prefix, indent) produce una salida legible. Úsalo para archivos de configuración y para depurar; las APIs suelen enviar JSON compacto.
Solo campos exportados
encoding/json usa reflexión y solo puede ver los campos exportados. Es el bug de JSON más común en Go:
type point struct {
x, y int // lowercase: json.Marshal(point{1, 2}) gives {}
}
Ni error ni aviso, solo {} a la salida y campos que se quedan a cero a la entrada. Pon los campos en mayúscula y añade tags para las claves en minúscula.
Cómo empareja los campos la decodificación
Al deserializar en un struct:
- Las claves se emparejan con el nombre del tag, o con el nombre del campo, sin distinguir mayúsculas y minúsculas.
{"NAME": "x"}rellena un campo con el tagjson:"name". - Las claves sin campo correspondiente se ignoran en silencio.
- Los campos sin clave correspondiente conservan su valor actual. Unmarshal no los reinicia, así que decodificar en un struct que ya tiene datos los mezcla.
- Un tipo que no coincide (un string donde el struct tiene un
int) devuelve un*json.UnmarshalTypeError, pero los demás campos se rellenan igualmente.
Para rechazar claves inesperadas, por ejemplo en una API estricta o en un archivo de configuración con erratas, usa un Decoder con DisallowUnknownFields:
Estructura desconocida: map[string]any
Cuando no conoces la forma de antemano, decodifica en map[string]any o en any. La correspondencia es fija:
| JSON | Go |
|---|---|
| objeto | map[string]any |
| array | []any |
| string | string |
| número | float64 |
| true / false | bool |
| null | nil |
En la salida se ven dos trampas. Todos los números son float64, así que m["stock"].(int) fallaría. Y los enteros por encima de 2^53 pierden precisión como float64: el id 9007199254740993 vuelve como ...992. UseNumber evita las dos.
Si conoces parte de la estructura, decodifica esa parte en un struct y usa json.RawMessage para el resto. Guarda los bytes en bruto de un campo para que puedas decodificarlo más tarde, cuando sepas su tipo.
Slices, maps, punteros y nil
| Valor Go | JSON |
|---|---|
slice nil (var s []int) | null |
slice vacío ([]int{}) | [] |
map nil | null |
puntero nil | null |
[]byte | un string en base64 |
map[string]T | un objeto, con las claves ordenadas |
map[int]T | un objeto con las claves enteras como strings |
La diferencia entre slice nil y slice vacío importa a los clientes de una API que esperan un array. Inicializa con []T{} o make([]T, 0) cuando el campo tenga que ser [].
Usa un campo puntero (*int, *bool) cuando necesites distinguir "ausente" de "cero" en la entrada. Tras deserializar, un puntero nil significa que la clave faltaba o era null; un puntero a 0 significa que el cliente envió 0.
Streams: Encoder y Decoder
json.Marshal y Unmarshal trabajan con slices de bytes completos. Para un io.Reader o un io.Writer (el body de una petición HTTP, un archivo, stdin), usa json.NewDecoder y json.NewEncoder. Un Decoder también puede leer una secuencia de valores JSON de uno en uno:
Encoder.Encode escribe un salto de línea detrás de cada valor. Por defecto, tanto Marshal como Encoder escapan <, > y & como \u003c, \u003e y \u0026, para que el JSON se pueda incrustar con seguridad en HTML. SetEscapeHTML(false) lo desactiva. En los handlers HTTP, json.NewDecoder(r.Body).Decode(&v) y json.NewEncoder(w).Encode(v) son la pareja habitual.
Codificación personalizada
Un tipo puede controlar su propio JSON implementando json.Marshaler y json.Unmarshaler. Un caso habitual es un enum con iota que debe aparecer como nombre, no como número:
MarshalJSON tiene un receptor por valor para que funcione tanto con valores como con punteros; UnmarshalJSON necesita un receptor puntero porque modifica el valor. Los tipos que solo necesitan una forma de string pueden implementar encoding.TextMarshaler (MarshalText), lo que además permite usarlos como claves de map.
Errores comunes
- Nombres de campo en minúscula. Se saltan en silencio.
- Pasar un valor a
Unmarshal. Necesita un puntero. - Ignorar el error. El JSON mal formado y los tipos que no coinciden solo se informan a través de él.
- Suponer que los números de
map[string]anysonint. Sonfloat64. - Esperar que
omitemptyquite un struct vacío o untime.Timea cero. Usaomitzeroen Go 1.24, o un puntero. - Depender del orden de los campos en los maps. Las claves de un map se ordenan en la salida; los campos de un struct mantienen su orden de declaración.
Preguntas frecuentes
¿Cómo convierto un struct a JSON en Go?
Llama a json.Marshal(v), que devuelve []byte y un error. Solo se incluyen los campos exportados (nombres que empiezan por mayúscula). Usa un struct tag como json:"name" en un campo para controlar su clave JSON. Para una salida con sangría usa json.MarshalIndent(v, "", " ").
¿Por qué faltan campos de mi struct en la salida JSON?
encoding/json solo ve los campos exportados. Un campo llamado name (en minúscula) le resulta invisible, tanto al serializar como al deserializar. Pon el campo en mayúscula y define la clave JSON con un tag como json:"name".
¿Qué hace omitempty en el JSON de Go?
omitempty deja un campo fuera de la salida cuando contiene un valor vacío: false, 0, "", un puntero o una interfaz nil, o un slice o un map vacíos. No trata como vacío un struct ni un time.Time. Go 1.24 añadió omitzero, que omite cualquier valor que sea el valor cero de su tipo (o cuyo método IsZero() devuelva true), incluidos los structs y time.Time.
¿Cómo parseo JSON con una estructura desconocida en Go?
Deserializa en map[string]any (o en any). Los objetos se convierten en map[string]any, los arrays en []any, los strings en string, los booleanos en bool y todos los números en float64. Usa aserciones de tipo para leer los valores, y Decoder.UseNumber si los enteros grandes tienen que mantenerse exactos.