Análisis de los cambios de comportamiento de encoding/json/v2 en Go 1.25 y de la capa de compatibilidad con v1

El paquete encoding/json v2 de Go es una nueva implementación destinada a mejorar varias deficiencias de la v1 (falta de consistencia, comportamientos sorprendentes y problemas de rendimiento). Se activa mediante la etiqueta de compilación goexperiment.jsonv2, lo que la convierte en una característica experimental.
Lo más importante es que cuando v2 está activado, v1 funciona como una capa de compatibilidad que emula el comportamiento de v1 sobre la implementación de v2. Esto se logra mediante la función DefaultOptionsV1() del archivo v2_v1.go. Es decir, v2 ofrece opciones que permiten reproducir por completo el comportamiento de v1, al mismo tiempo que presenta un nuevo comportamiento predeterminado más estricto y predecible.
Los objetivos principales de v2 son los siguientes:
- Mejora de la corrección y la previsibilidad: aplica por defecto reglas más estrictas (por ejemplo, distinción entre mayúsculas y minúsculas y prohibición de claves duplicadas) para reducir los comportamientos inesperados.
- Mejora del rendimiento: se rediseñaron los motores de análisis y codificación para aumentar la eficiencia.
- Mayor flexibilidad y control: se introduce un sistema detallado de opciones que permite al desarrollador controlar con precisión el procesamiento de JSON.
Principales diferencias de semántica y comportamiento
Se resumen por elementos las diferencias de comportamiento entre v1 y v2, tomando como referencia el archivo v2_test.go.
1. Coincidencia de nombres de campo (distinción entre mayúsculas y minúsculas)
- Comportamiento de v1: al deserializar (unmarshal) los miembros de un objeto JSON en los campos de una estructura de Go, la coincidencia ignora las mayúsculas y minúsculas (case-insensitive). Tanto
"FirstName"como"firstname"se asignan al campoFirstName. - Comportamiento de v2: por defecto distingue entre mayúsculas y minúsculas (case-sensitive) y solo asigna los campos que coinciden exactamente.
- Motivo del cambio: la coincidencia sin distinción de mayúsculas puede ser causa de comportamientos inesperados y provoca una degradación del rendimiento al procesar campos que no coinciden. v2 adopta como comportamiento predeterminado uno más claro y predecible.
- Opciones relacionadas: en v2 se puede activar explícitamente la coincidencia sin distinción de mayúsculas para un campo concreto mediante la opción de etiqueta
json:"...,case:ignore", o aplicarla de forma global conjson.MatchCaseInsensitiveNames(true).
2. Cambio de significado de la opción de etiqueta omitempty
- Comportamiento de v1: omite el campo según el "estado vacío" del valor de Go. En este caso, el "estado vacío" significa
false,0, punteros o interfacesnil, y arreglos, slices, mapas o cadenas de longitud 0. - Comportamiento de v2: omite el campo según el "estado vacío" del valor JSON codificado. Es decir, se omite cuando se codifica como
null,"",{}o[]. - Motivo del cambio: la definición de v1 depende del sistema de tipos de Go. v2 se basa en el sistema de tipos de JSON, lo que proporciona un comportamiento más consistente. Por ejemplo, en v1 se omite el valor
falsede un tipobool, mientras que en v2falsees un valor JSON no vacío y, por tanto, no se omite. En v2 se añade la opciónomitzero, que puede reemplazar el comportamiento deomitemptyde v1 aplicado a0ofalse. - Opciones relacionadas: para obtener en v2 el mismo comportamiento que en v1, se usa la opción
json.OmitEmptyWithLegacyDefinition(true).
3. Cambio de comportamiento de la opción de etiqueta string
- Comportamiento de v1: se aplica a campos de tipo numérico, booleano y cadena. Codifica de nuevo el valor dentro de una cadena JSON (por ejemplo,
int(42)->"42"). No se aplica de forma recursiva a los valores dentro de tipos compuestos (slices, mapas, etc.). - Comportamiento de v2: se aplica únicamente a tipos numéricos y lo hace de forma recursiva. Es decir, los números dentro de un slice como
[]inttambién se codifican como cadenas JSON. - Motivo del cambio: el uso principal de la opción
stringes representar enteros de 64 bits como cadenas para evitar la pérdida de precisión. El comportamiento de v1 era limitado e inconsistente. v2 se centra en este uso principal y amplía el comportamiento de forma recursiva para hacerlo más útil. - Opciones relacionadas: la opción
json.StringifyWithLegacySemantics(true)permite imitar el comportamiento de v1.
4. Serialización (marshal) de slices y mapas nil
- Comportamiento de v1: los slices
nily los mapasnilse serializan comonull. - Comportamiento de v2: por defecto, los slices
nilse serializan como[](arreglo vacío) y los mapasnilcomo{}(objeto vacío). - Motivo del cambio:
niles un detalle de implementación del lenguaje Go, y no resulta conveniente exponerlo en un formato JSON independiente del lenguaje.[]y{}son representaciones más habituales de colecciones vacías. - Opciones relacionadas: en v2, las opciones
json.FormatNilSliceAsNull(true)ojson.FormatNilMapAsNull(true)permiten serializar comonull, igual que en v1.
5. Deserialización (unmarshal) de arreglos
- Comportamiento de v1: al deserializar en un arreglo de Go (
[N]T), no se produce error aunque la longitud del arreglo JSON sea distinta de la del arreglo de Go. Si el arreglo JSON es más corto, el espacio sobrante se rellena con valores cero; si es más largo, el excedente se descarta. - Comportamiento de v2: la longitud del arreglo JSON debe coincidir exactamente con la del arreglo de Go. En caso contrario, se produce un error.
- Motivo del cambio: en Go, la longitud de los arreglos de tamaño fijo suele tener un significado importante. El comportamiento de v1 puede provocar pérdidas silenciosas de datos. v2 aplica reglas más estrictas para mejorar la corrección.
- Opciones relacionadas: la opción
json.UnmarshalArrayFromAnyLength(true)permite imitar el comportamiento de v1.
6. Tratamiento de time.Duration
- Comportamiento de v1:
time.Durationse trata internamente comoint64y se codifica como un número JSON en nanosegundos. - Comportamiento de v2: se codifica como una cadena JSON con un formato como
"1h2m3s", mediante el métodotime.Duration.String(). - Motivo del cambio: los nanosegundos numéricos son poco legibles, y la representación textual estándar de
time.Durationresulta más útil. - Opciones relacionadas: se puede usar el comportamiento de v1 mediante la opción de etiqueta
json:",format:nano"o la opciónjson.FormatTimeWithLegacySemantics(true).
7. Tratamiento de UTF-8 no válido
- Comportamiento de v1: durante la serialización o deserialización, si una cadena contiene bytes UTF-8 no válidos, estos se reemplazan silenciosamente por el carácter de reemplazo Unicode (
\uFFFD). - Comportamiento de v2: por defecto, al encontrar UTF-8 no válido devuelve un error.
- Motivo del cambio: evitar la corrupción silenciosa de datos y cumplir con el estándar JSON más estricto (RFC 7493).
- Opciones relacionadas: la opción
jsontext.AllowInvalidUTF8(true)permite imitar el comportamiento de v1.
8. Tratamiento de nombres de miembros duplicados en objetos
- Comportamiento de v1: permite que aparezcan miembros con el mismo nombre dentro de un objeto JSON. Prevalece el último valor.
- Comportamiento de v2: por defecto, si existen nombres de miembros duplicados, devuelve un error.
- Motivo del cambio: el estándar RFC 8259 no define el comportamiento para nombres duplicados, por lo que las implementaciones pueden comportarse de forma distinta. Esto puede ser una causa de vulnerabilidades de seguridad. v2 rechaza explícitamente estos casos para mejorar la corrección y la seguridad.
- Opciones relacionadas: la opción
jsontext.AllowDuplicateNames(true)permite imitar el comportamiento de v1.
Diferencias de implementación y arquitectura
- v1: depende en gran medida de
decodeStateendecode.goy de la máquina de estados escrita manualmente enscanner.go. Se trata de una estructura monolítica en la que el análisis sintáctico y el análisis semántico están fuertemente acoplados. - v2: la arquitectura es más modular.
encoding/json/jsontext: proporciona un tokenizadorDecodery un codificadorEncoderde JSON de bajo nivel y alto rendimiento. Este paquete se centra únicamente en los aspectos sintácticos de JSON.encoding/json/v2: se basa enjsontextpara gestionar la conversión semántica entre los tipos de Go y los valores JSON.- Gracias a esta separación, el análisis sintáctico y el semántico quedan desacoplados, lo que mejora la claridad del código y el rendimiento.
Nuevas API y funciones de v2
v2 ofrece un control muy flexible mediante el sistema json.Options.
json.Options: conjunto de opciones que modifican el comportamiento de la serialización y deserialización.json.JoinOptions(...): combina varias opciones en una sola.WithMarshalers/WithUnmarshalers: función potente que permite inyectar la lógica de serialización y deserialización para tipos concretos sin implementar las interfacesMarshaleroUnmarshaler. Resulta especialmente útil al tratar tipos de paquetes externos.- Nuevas opciones:
RejectUnknownMembers,Deterministic(false),FormatNilSliceAsNull, entre otras, permiten controlar comportamientos que en v1 no eran posibles.
Conclusión
encoding/json v2 es una implementación moderna que, a partir de la experiencia con v1, mejora notablemente la corrección, el rendimiento y la flexibilidad. Su comportamiento predeterminado es más estricto, pero gracias a un sistema de Options detallado es posible reproducir por completo el comportamiento de v1, lo que permite mantener la compatibilidad con el código existente e incorporar gradualmente las ventajas de v2.
- En proyectos nuevos, se recomienda usar v2 como opción predeterminada.
- En proyectos existentes, es posible seguir usando
jsonv1tal cual, o migrar ajsonv2e incorporar gradualmente el comportamiento más estricto de v2 medianteDefaultOptionsV1().