Menu

JSON en Golang: Marshal, Unmarshal y struct tags

Cómo codificar y decodificar JSON en Go con encoding/json: Marshal y Unmarshal, struct tags como omitempty y omitzero, salida con sangría, decodificar en map[string]any, rechazar campos desconocidos y streaming con Decoder.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

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".

TagEfecto
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 tagla 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 tag json:"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:

JSONGo
objetomap[string]any
array[]any
stringstring
númerofloat64
true / falsebool
nullnil

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 GoJSON
slice nil (var s []int)null
slice vacío ([]int{})[]
map nilnull
puntero nilnull
[]byteun string en base64
map[string]Tun objeto, con las claves ordenadas
map[int]Tun 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]any son int. Son float64.
  • Esperar que omitempty quite un struct vacío o un time.Time a cero. Usa omitzero en 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.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR