Jorge Turbica / blog · Blog ← Todos los artículos EN · Read in English
Lenguajes

Go 1.27 ya ejecuta tu encoding/json sobre v2: Unmarshal hasta 10 veces más rápido

encoding/json/v2 es estable en Go 1.27: más rápido y más estricto con nil, UTF-8 y claves duplicadas. Cómo migrar sin romper tu API.

Desde Go 1.27, cada json.Unmarshal de tu código ya pasa por la implementación de encoding/json/v2, aunque no hayas cambiado un solo import. El nuevo paquete y su base sintáctica, encoding/json/jsontext, son estables tras seis años de trabajo, y el equipo de Go publicó el 4 de noviembre de 2026 la explicación completa y una guía de migración. La promesa: deserializar hasta 10 veces más rápido; el precio: unos valores por defecto más estrictos que pueden cambiar lo que tu servicio devuelve.

Lo que ya ha cambiado sin tocar nada

Go 1.27 salió el 19 de agosto de 2026 y la versión actual es la 1.27.3. Sus notas de versión dicen algo que se lee rápido y pesa mucho: el encoding/json de siempre ahora se apoya en la implementación de v2. El comportamiento de Marshal y Unmarshal se conserva, pero el texto exacto de los mensajes de error puede cambiar. Si tienes tests que comparan cadenas de error, o alertas que buscan un mensaje concreto en los logs, ahí está el primer sitio donde mirar al actualizar.

Si algo se rompe, existe una salida de emergencia: compilar con GOEXPERIMENT=nojsonv2 recupera la implementación antigua. Las notas avisan de que esta opción desaparecerá en una versión futura, así que sirve para ganar tiempo, no para quedarse.

bash
# Build with the old v1 implementation while you investigate
GOEXPERIMENT=nojsonv2 go build ./...

Por qué hacía falta un v2

encoding/json acumulaba problemas que no se podían arreglar sin romper la promesa de compatibilidad de Go 1. Algunos son de seguridad: acepta UTF-8 inválido y lo sustituye en silencio, acepta objetos con claves duplicadas (un vector ya explotado en CVE-2017-12635) y empareja los nombres de campo sin distinguir mayúsculas. Otros son de rendimiento: MarshalJSON obliga a reservar un []byte y a revalidarlo, UnmarshalJSON exige parsear el valor dos veces, y si un método recursa en Unmarshal el coste se vuelve cuadrático.

El caso más claro que cita el equipo de Go es de Kubernetes: el parseo recursivo de especificaciones OpenAPI dentro de UnmarshalJSON lastraba un servicio, y pasar a UnmarshalJSONFrom lo mejoró en órdenes de magnitud.

La solución separa dos capas. jsontext trabaja solo con la sintaxis (tokens y valores, en streaming real y sin reflexión), y json/v2 asigna significado a esos datos. json.RawMessage es ahora un alias de jsontext.Value.

v1 frente a v2: los cambios de comportamiento

Casoencoding/json (v1)encoding/json/v2
UTF-8 inválidoLo sustituye en silencioError
Claves duplicadasLas aceptaError
Slice o map nilnull[] o {}
Nombres de campo al deserializarSin distinguir mayúsculasDistingue mayúsculas
omitemptySegún el valor GoOmite si el JSON sería null, "", [] o {}
Tags mal formadosSe ignoranError en tiempo de ejecución

A eso se suma time.Duration: v2 da error al serializarlo porque no tiene una representación por defecto, y te deja elegir el formato con opciones.

El de los nil es el que más guerra puede dar. Un struct con Nicknames []string sin inicializar pasa de serializarse como "Nicknames":null a "Nicknames":[]. Para un servicio nuevo es mejor, pero si un cliente móvil o un consumidor en otro lenguaje distingue entre null y lista vacía, acabas de cambiar tu contrato de API.

En rendimiento, Marshal queda más o menos igual que en v1 y Unmarshal es mucho más rápido, con mejoras de hasta 10 veces en las pruebas del equipo. Para aprovecharlo del todo, los tipos con serialización propia deben implementar también MarshalJSONTo y UnmarshalJSONFrom, que trabajan sobre el Encoder y el Decoder en streaming.

Cómo migrar a encoding/json/v2

La API es casi idéntica: b, err := json.Marshal(v) compila igual cambiando el import a encoding/json/v2. Lo delicado no es compilar, es que la salida cambie sin que nadie se entere. La guía oficial propone tres caminos según el riesgo que aceptes:

  • Todo de golpe, para aplicaciones sencillas: cambiar los imports, pasar los tests y ajustar opciones donde falle.
  • Opción a opción: migrar todas las llamadas a v2 con jsonv1.DefaultOptionsV1(), que se comporta igual que v1, y activar después cada comportamiento de v2 por separado.
  • Con jsonsplit en producción: un envoltorio que serializa con v1 y v2 a la vez, devuelve el resultado de v1 y te informa de cada diferencia, incluso de qué opción la provoca.

El segundo camino se ve así. Las opciones posteriores sobrescriben a las anteriores:

go
import (
	jsonv1 "encoding/json"
	"encoding/json/v2"
)

// Behaves like v1, except that nil slices become []
b, err := json.Marshal(v, jsonv1.DefaultOptionsV1(), json.FormatNilSliceAsNull(false))

Y jsonsplit se configura una vez al arrancar:

go
func init() {
	jsonsplit.GlobalCodec.SetMarshalCallMode(jsonsplit.CallBothButReturnV1)
	jsonsplit.GlobalCodec.AutoDetectOptions = true
	jsonsplit.GlobalCodec.ReportDifference = func(d jsonsplit.Difference) {
		slog.Warn("json v1/v2 difference", "diff", d)
	}
}

Para un servicio de producción, este es el que yo elegiría: envías las diferencias a tus logs o métricas, dejas correr el tráfico real unos días y solo cambias el modo a OnlyCallV2 o CallBothButReturnV2 cuando dejen de aparecer. El coste es real: serializar dos veces duplica el trabajo de JSON, y la autodetección de opciones añade más. SetMarshalCallRatio permite muestrear solo una fracción de las llamadas.

Si ya habías probado v2, revisa ese código: durante el experimento, su API cambió. Desaparecieron la opción de tag format, la opción unknown y DiscardUnknownMembers; inline pasó a llamarse embed, y los accesos numéricos de jsontext.Token ahora también devuelven error. Afecta a quien lo usó con el experimento de Go 1.25 o con el módulo github.com/go-json-experiment/json.

Checklist antes de producción

  • Actualiza a Go 1.27.3 y ejecuta los tests sin cambiar imports: detecta comparaciones con mensajes de error que hayan cambiado.
  • Busca en logs, alertas y tests cadenas literales de errores de encoding/json.
  • Antes de pasar un servicio a v2, revisa los contratos con consumidores externos: null frente a [], campos con distinta capitalización y time.Duration.
  • Usa jsonsplit con muestreo en producción y no cambies de modo hasta que dejen de llegar diferencias.
  • En tipos con MarshalJSON o UnmarshalJSON pesados, implementa MarshalJSONTo y UnmarshalJSONFrom para que el streaming se note.

No hay prisa: v1 seguirá funcionando indefinidamente y nadie te obliga a migrar. Pero el rechazo de claves duplicadas y de UTF-8 inválido cierra problemas de seguridad reales, y en servicios que deserializan mucho JSON la diferencia de rendimiento se nota en la factura de CPU.

Fuentes

Jorge Turbica

Software Engineer y SRE. Escribo sobre los retos reales que me encuentro en producción.

Ver portfolio ↗