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.
# 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
| Caso | encoding/json (v1) | encoding/json/v2 |
|---|---|---|
| UTF-8 inválido | Lo sustituye en silencio | Error |
| Claves duplicadas | Las acepta | Error |
Slice o map nil | null | [] o {} |
| Nombres de campo al deserializar | Sin distinguir mayúsculas | Distingue mayúsculas |
omitempty | Según el valor Go | Omite si el JSON sería null, "", [] o {} |
| Tags mal formados | Se ignoran | Error 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
jsonspliten 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:
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:
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:
nullfrente a[], campos con distinta capitalización ytime.Duration. - Usa
jsonsplitcon muestreo en producción y no cambies de modo hasta que dejen de llegar diferencias. - En tipos con
MarshalJSONoUnmarshalJSONpesados, implementaMarshalJSONToyUnmarshalJSONFrompara 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
- A new Go API for JSON, Go Blog, 4 de noviembre de 2026.
- Go 1.27 Release Notes, go.dev.
- encoding/json/v2 Migration Guide, go.dev.
- Release History, go.dev.