GoSuda

Comparación de encoding/json v1 vs v2 en Go 1.25

By lemonmint
views ...

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:

  1. 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.
  2. Mejora del rendimiento: El motor de análisis y codificación ha sido rediseñado para aumentar la eficiencia.
  3. 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 campo FirstName.
  • 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ón json.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/interfaces nil, 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 tipo bool 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ón omitzero para reemplazar el comportamiento de omitempty de la v1 que se aplicaba a 0 o false.
  • 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 maps nil se marshalizan como null.
  • Comportamiento de la v2: Por defecto, los slices nil se marshalizan como [] (arreglo vacío) y los maps nil 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 opciones json.FormatNilSliceAsNull(true) o json.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 como int64 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étodo time.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ón json.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 en decode.go y de una máquina de estados escrita manualmente en scanner.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 en jsontext 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 interfaces Marshaler/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 a jsonv2, adoptando gradualmente el comportamiento estricto de la v2 a través de DefaultOptionsV1().