Comparación de encoding/json v1 vs v2 en Go 1.25
La versión 2 del paquete encoding/json
de Go es una nueva implementación diseñada para mejorar varias deficiencias de la v1 original (falta de consistencia, comportamiento inesperado, problemas de rendimiento). Esta es una característica experimental que se activa mediante la etiqueta de compilación goexperiment.jsonv2
.
El punto más crucial es que cuando se activa la v2, la v1 opera como una capa de compatibilidad que emula el comportamiento de la v1 sobre la implementación de la v2. Esto se logra a través de la función DefaultOptionsV1()
en el archivo v2_v1.go
. Es decir, la v2 proporciona opciones que permiten reproducir completamente el comportamiento de la v1, al mismo tiempo que introduce un nuevo comportamiento predeterminado más estricto y predecible.
Los objetivos principales de la v2 son los siguientes:
- Mejora de la precisión y la previsibilidad: Aplica reglas más estrictas por defecto (p. ej., distinción entre mayúsculas y minúsculas, prohibición de claves duplicadas) para reducir comportamientos inesperados.
- Mejora del rendimiento: El motor de análisis y codificación ha sido rediseñado para aumentar la eficiencia.
- Mayor flexibilidad y control: Se ha introducido un sistema de opciones detallado que permite a los desarrolladores controlar finamente cómo se procesa JSON.
Diferencias clave en el significado/comportamiento
Se han organizado las diferencias de comportamiento entre la v1 y la v2 por elementos, centrándose en el archivo v2_test.go
.
1. Coincidencia de nombres de campo (distinción entre mayúsculas y minúsculas)
- Comportamiento de la v1: Al desmarshalizar miembros de objetos JSON a campos de estructuras Go, la coincidencia se realiza sin distinguir entre mayúsculas y minúsculas (case-insensitive). Tanto
"FirstName"
como"firstname"
se mapean al campoFirstName
. - Comportamiento de la v2: Por defecto, se distingue entre mayúsculas y minúsculas (case-sensitive), mapeando solo los campos que coinciden exactamente.
- Razón del cambio: La coincidencia sin distinción entre mayúsculas y minúsculas puede ser una fuente de comportamiento inesperado y puede causar una degradación del rendimiento al procesar campos no coincidentes. La v2 ha adoptado un comportamiento predeterminado más claro y predecible.
- Opciones relacionadas: En la v2, se puede usar la opción de etiqueta
json:"...,case:ignore"
para habilitar explícitamente la distinción de mayúsculas y minúsculas por campo, o aplicar la opciónjson.MatchCaseInsensitiveNames(true)
globalmente.
2. Cambio en el significado de la opción de etiqueta omitempty
- Comportamiento de la v1: Omite el campo basándose en el "estado vacío" del valor Go. Aquí, "estado vacío" significa
false
,0
, punteros/interfacesnil
, y arreglos/slices/maps/cadenas con longitud 0. - Comportamiento de la v2: Omite el campo basándose en el "estado vacío" del valor JSON codificado. Es decir, se omite si se codifica como
null
,""
,{}
,[]
. - Razón del cambio: La definición de la v1 depende del sistema de tipos de Go. La v2 proporciona un comportamiento más consistente al basarse en el sistema de tipos JSON. Por ejemplo, en la v1, el valor
false
de tipobool
se omite, pero en la v2,false
no es un valor JSON vacío, por lo que no se omite. La v2 ha añadido la opciónomitzero
para reemplazar el comportamiento deomitempty
de la v1 que se aplicaba a0
ofalse
. - Opciones relacionadas: Si se desea el mismo comportamiento que la v1 en la v2, se utiliza la opción
json.OmitEmptyWithLegacyDefinition(true)
.
3. Cambio en el comportamiento de la opción de etiqueta string
- Comportamiento de la v1: Se aplica a campos de tipo numérico, booleano y cadena. Codifica el valor dentro de una cadena JSON (ej:
int(42)
->"42"
). No se aplica recursivamente a valores dentro de tipos compuestos (slices, maps, etc.). - Comportamiento de la v2: Se aplica únicamente a tipos numéricos y de forma recursiva. Es decir, los números dentro de slices como
[]int
también se codifican como cadenas JSON. - Razón del cambio: El propósito principal de la opción
string
es representar números como cadenas para evitar la pérdida de precisión de enteros de 64 bits. El comportamiento de la v1 era limitado y carecía de consistencia. La v2 se centra en este uso principal y extiende el comportamiento recursivamente para hacerlo más útil. - Opciones relacionadas: La opción
json.StringifyWithLegacySemantics(true)
puede emular el comportamiento de la v1.
4. Marshalling de slices y maps nil
- Comportamiento de la v1: Los slices
nil
y los mapsnil
se marshalizan comonull
. - Comportamiento de la v2: Por defecto, los slices
nil
se marshalizan como[]
(arreglo vacío) y los mapsnil
como{}
(objeto vacío). - Razón del cambio:
nil
es un detalle de implementación del lenguaje Go, y no es deseable exponerlo en el formato JSON, que es independiente del lenguaje. Las colecciones vacías representadas por[]
o{}
son una expresión más universal. - Opciones relacionadas: En la v2, se puede marshalizar como
null
al igual que en la v1 mediante las opcionesjson.FormatNilSliceAsNull(true)
ojson.FormatNilMapAsNull(true)
.
5. Unmarshalling de arreglos
- Comportamiento de la v1: Al desmarshalizar a un arreglo Go (
[N]T
), no genera un error incluso si la longitud del arreglo JSON difiere de la longitud del arreglo Go. Si la longitud es menor, el espacio restante se llena con valores cero; si es mayor, el excedente se descarta. - Comportamiento de la v2: La longitud del arreglo JSON debe coincidir exactamente con la longitud del arreglo Go. De lo contrario, se generará un error.
- Razón del cambio: En Go, los arreglos de tamaño fijo a menudo tienen una longitud que es significativamente importante. El comportamiento de la v1 podría provocar una pérdida silenciosa de datos. La v2 aumenta la precisión con reglas más estrictas.
- Opciones relacionadas: La opción
json.UnmarshalArrayFromAnyLength(true)
puede emular el comportamiento de la v1.
6. Manejo de time.Duration
- Comportamiento de la v1:
time.Duration
se trata internamente comoint64
y se codifica como un número JSON en unidades de nanosegundos. - Comportamiento de la v2: Se codifica como una cadena JSON en el formato
"1h2m3s"
utilizando el métodotime.Duration.String()
. - Razón del cambio: Los nanosegundos numéricos son difíciles de leer, y la representación de cadena estándar de
time.Duration
es más útil. - Opciones relacionadas: Se puede utilizar el comportamiento de la v1 a través de la opción de etiqueta
json:",format:nano"
o la opciónjson.FormatTimeWithLegacySemantics(true)
.
7. Manejo de UTF-8 inválido
- Comportamiento de la v1: Al marshalizar/unmarshalizar, si hay bytes UTF-8 inválidos dentro de una cadena, se reemplazan silenciosamente con el carácter de reemplazo Unicode (
\uFFFD
). - Comportamiento de la v2: Por defecto, si encuentra UTF-8 inválido, devuelve un error.
- Razón del cambio: Para evitar la corrupción silenciosa de datos y para adherirse a un estándar JSON más estricto (RFC 7493).
- Opciones relacionadas: La opción
jsontext.AllowInvalidUTF8(true)
puede emular el comportamiento de la v1.
8. Manejo de nombres de miembros de objetos duplicados
- Comportamiento de la v1: Permite la aparición de miembros con el mismo nombre duplicados dentro de un objeto JSON. El valor que aparece en último lugar sobrescribe los anteriores.
- Comportamiento de la v2: Por defecto, si hay nombres de miembros duplicados, devuelve un error.
- Razón del cambio: El estándar RFC 8259 no define el comportamiento de los nombres duplicados, lo que puede llevar a que las implementaciones se comporten de manera diferente. Esto puede ser una fuente de vulnerabilidades de seguridad. La v2 lo rechaza explícitamente para aumentar la precisión y la seguridad.
- Opciones relacionadas: La opción
jsontext.AllowDuplicateNames(true)
puede emular el comportamiento de la v1.
Diferencias de implementación y arquitectura
- v1: Depende en gran medida de
decodeState
endecode.go
y de una máquina de estados escrita manualmente enscanner.go
. Esta es una estructura monolítica donde la lógica de análisis y la semántica están fuertemente acopladas. - v2: La arquitectura es más modular.
encoding/json/jsontext
: Proporciona un tokenizador JSON (Decoder
) y un codificador (Encoder
) de bajo nivel y alto rendimiento. Este paquete se centra únicamente en los aspectos sintácticos de JSON.encoding/json/v2
: Se basa enjsontext
para manejar las transformaciones semánticas entre tipos Go y valores JSON.- Esta separación permite que el análisis sintáctico y semántico estén desacoplados, mejorando la claridad del código y el rendimiento.
Nuevas API y funcionalidades de la v2
La v2 ofrece un control muy flexible a través del sistema json.Options
.
json.Options
: Es un conjunto de opciones que modifican el comportamiento de marshalling/unmarshalling.json.JoinOptions(...)
: Combina varias opciones en una sola.WithMarshalers
/WithUnmarshalers
: Una potente funcionalidad que permite inyectar lógica de serialización/deserialización para tipos específicos sin necesidad de implementar las interfacesMarshaler
/Unmarshaler
. Esto es particularmente útil al procesar tipos de paquetes externos.- Nuevas opciones:
RejectUnknownMembers
,Deterministic(false)
,FormatNilSliceAsNull
, entre otras, permiten un control de comportamiento diverso que no era posible en la v1.
Conclusión
encoding/json
v2 es una implementación moderna que, basada en la experiencia de la v1, ha mejorado significativamente la precisión, el rendimiento y la flexibilidad. Aunque el comportamiento predeterminado es más estricto, el sofisticado sistema Options
soporta completamente todas las operaciones de la v1, permitiendo la introducción gradual de las ventajas de la v2 mientras se mantiene la compatibilidad con el código existente.
- Si es un proyecto nuevo, se recomienda utilizar la v2 por defecto.
- Los proyectos existentes pueden seguir utilizando
jsonv1
o migrar ajsonv2
, adoptando gradualmente el comportamiento estricto de la v2 a través deDefaultOptionsV1()
.