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:
| Caso | v1 | v2 |
|---|---|---|
| UTF-8 inválido | Lo acepta en silencio | Devuelve un error |
| Claves duplicadas en un objeto | Se queda con la última | Devuelve un error |
Slice o map nil | null | [] o {} |
| Nombres de campos al deserializar | No distingue mayúsculas | Sí las distingue |
omitempty | Omite valores “vacíos” de Go (0, false, ""…) | Omite lo que se codificaría como null, "", [] o {} |
time.Duration | Lo codifica como número de nanosegundos | Devuelve 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:
- Actualizar a Go 1.27 sin tocar nada y comprobar que los tests pasan.
- En código nuevo, usar
encoding/json/v2directamente. - En el código antiguo, migrar por partes, empezando por
Unmarshalen las rutas donde el rendimiento importe. - 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.




