Backend 5 min de lectura 935 palabras

encoding/json/v2 ya es estable en Go 1.27: qué cambia y cómo migrar

EN
encoding/json/v2 ya es estable en Go 1.27: qué cambia y cómo migrar

El equipo de Go ha publicado A new Go API for JSON, donde anuncia que los paquetes encoding/json/v2 y encoding/json/jsontext son estables a partir de Go 1.27. Es la primera vez que la biblioteca estándar de Go saca una versión 2 de un paquete tan usado como encoding/json, y el proceso ha llevado unos seis años: una charla en la GopherCon a finales de 2023, una propuesta formal a principios de 2025 y un periodo como experimento en Go 1.25.

Hace unas semanas escribía sobre otro cambio de Go 1.27, la reutilización de conexiones HTTP/1, que mejoraba el rendimiento sin tocar el código. Este es distinto: si quieres aprovecharlo, sí hay que tocar algo, aunque el equipo de Go ha hecho que se pueda migrar poco a poco.

Por qué una v2

encoding/json es de los paquetes más usados de Go, y también de los que más quejas acumulaban. Algunos de sus comportamientos son discutibles, pero no se podían cambiar sin romper programas que llevan años funcionando. La solución ha sido sacar una versión nueva con valores por defecto más estrictos y dejar la antigua tal cual.

Lo que cambia

Estas son las diferencias de comportamiento que destaca el artículo:

Casov1v2
UTF-8 inválidoLo acepta en silencioDevuelve un error
Claves duplicadas en un objetoSe queda con la últimaDevuelve un error
Slice o map nilnull[] o {}
Nombres de campos al deserializarNo distingue mayúsculasSí las distingue
omitemptyOmite valores “vacíos” de Go (0, false, ""…)Omite lo que se codificaría como null, "", [] o {}
time.DurationLo codifica como número de nanosegundosDevuelve un error, salvo que indiques el formato

La que más me gusta es la de los nil. Cualquiera que haya escrito una API en Go se ha encontrado alguna vez con un cliente en JavaScript que falla porque esperaba un array y recibió null:

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

type Usuario struct {
    Nombre string   `json:"nombre"`
    Roles  []string `json:"roles"`
}

u := Usuario{Nombre: "Ana"}

b1, _ := jsonv1.Marshal(u) // {"nombre":"Ana","roles":null}
b2, _ := json.Marshal(u)   // {"nombre":"Ana","roles":[]}

La de las mayúsculas es la que más puede sorprender al migrar. Con v1, un JSON con "Nombre" rellena el campo con la etiqueta nombre; con v2 no, y el campo se queda vacío sin ningún error. Si tus clientes envían nombres de campo con mayúsculas inconsistentes, es lo primero que hay que revisar.

Y la de las claves duplicadas tiene más que ver con la seguridad de lo que parece: si dos partes de un sistema interpretan un mismo JSON de forma distinta (una se queda con la primera clave y otra con la última), se abre la puerta a saltarse validaciones.

Opciones en vez de variables globales

El otro cambio importante es de diseño. En v1, para personalizar algo había que usar métodos en los tipos o configurar un Encoder. En v2, las opciones son un argumento más de Marshal y Unmarshal, y se pueden combinar. Esto permite, por ejemplo, usar todo el comportamiento de v2 salvo una cosa:

// Todo como en v2, pero permitiendo claves duplicadas
json.Marshal(v, json.DefaultOptionsV2(), jsontext.AllowDuplicateName(true))

// Todo como en v1, pero rechazando claves duplicadas
json.Marshal(v, jsonv1.DefaultOptionsV1(), jsontext.AllowDuplicateName(false))

También hay opciones para pasar funciones de serialización propias en la llamada, como WithMarshalers y WithUnmarshalers, sin tener que definir métodos en los tipos. Es muy útil cuando el tipo no es tuyo y no puedes añadirle un MarshalJSON.

El paquete jsontext se encarga de la parte de bajo nivel: leer y escribir tokens JSON en streaming, sin pasar por la reflexión. Para eso están los métodos MarshalJSONTo y UnmarshalJSONFrom, que trabajan directamente sobre el flujo en vez de generar un []byte intermedio.

Rendimiento

Según el artículo, Marshal va más o menos igual que en v1, a veces un poco más rápido y a veces un poco más lento. Donde se nota es en Unmarshal, con mejoras de hasta 10 veces. Mencionan además un servicio de Kubernetes que, al pasar a UnmarshalJSONFrom, mejoró “en órdenes de magnitud”. Y anuncian más optimizaciones para Go 1.28.

Cómo migrar

Lo mejor del anuncio es que no hay que migrar si no quieres. encoding/json v1 seguirá soportado de forma indefinida, y ahora está implementado sobre v2 con todas las opciones de compatibilidad activadas. Eso significa dos cosas: tu código actual sigue funcionando igual, y las mejoras que lleguen a v2 (nuevos métodos, nuevas etiquetas) también llegan a v1.

Para un proyecto existente, lo que yo haría:

  1. Actualizar a Go 1.27 sin tocar nada y comprobar que los tests pasan.
  2. En código nuevo, usar encoding/json/v2 directamente.
  3. En el código antiguo, migrar por partes, empezando por Unmarshal en las rutas donde el rendimiento importe.
  4. Si algo se rompe, usar las opciones para recuperar el comportamiento de v1 sólo en ese punto, en vez de dar marcha atrás.

El equipo de Go ha publicado una guía de migración en la documentación del paquete con todos los detalles, incluidas las nuevas opciones de las etiquetas de los structs.

Lo que me llevo

Me parece un buen ejemplo de cómo evolucionar una API muy usada sin romper a nadie: una versión nueva con mejores valores por defecto, la antigua construida encima de la nueva y opciones para mezclar ambas mientras dura la transición. Seis años es mucho tiempo, pero en un paquete que está en casi cualquier programa Go del mundo, prefiero esa prudencia a un cambio rápido que obligue a revisar todo de golpe.

Fuente: A new Go API for JSON, Michael Pratt, Joe Tsai, Daniel Martí, Johan Brandhorst-Satzkorn, Roger Peppe, Chris Hines y Damien Neil, blog de Go.