Cuidados Esenciales en Go: Cómo utilizar las funciones Marshal() y Unmarshal() de manera segura para JSON

augustoasilva

Augusto Silva

Posted on September 9, 2023

Cuidados Esenciales en Go: Cómo utilizar las funciones Marshal() y Unmarshal() de manera segura para JSON

Recientemente, el líder técnico de mi equipo encontró un error relacionado con **Unmarshal()** en Go. Nos mostró lo que sucedió y luego discutimos el riesgo de que esto ocurriera. Esto me dejó intrigado y decidí investigar más sobre lo que puede suceder cuando no se toman las precauciones adecuadas al manipular JSON en un código de Go.

Por lo tanto, comencemos hablando un poco sobre las funciones **Marshal()** y **Unmarshal()** en Go, y luego mostraré errores que pueden ocurrir si no tomamos las precauciones adecuadas. ¿Listos?

Manipulación nativa de JSON en Go: Marshal y Unmarshal

Cuando se trata de manipular datos en formato JSON en el lenguaje de programación Go, las funciones **Marshal()** y **Unmarshal()** son herramientas poderosas que permiten la conversión entre una estructura (o estructuras, si lo prefiere) en JSON, así como de JSON a estructura, respectivamente. Sin embargo, es importante comprender los posibles peligros asociados con estas operaciones y adoptar algunas prácticas para evitar errores sutiles que pueden ahorrarle horas de depuración.

Error 1: Olvidar exportar campos

Comenzando con errores comunes, este es bastante fácil de resolver, pero puede ser un poco complicado de encontrar si no se tiene cuidado. Recuerde que en Go, la visibilidad de una variable, función, método o campo se determina por la primera letra de su nombre, siendo minúscula o mayúscula, lo que indica que es privado o público, respectivamente. Por lo tanto, al trabajar con **Marshal()** y **Unmarshal()**, recuerde que solo los campos exportados, es decir, públicos (comenzando con una letra mayúscula) en una estructura serán considerados. Los campos no exportados no se incluirán en la serialización (conversión del objeto/estructura a JSON) o se completarán con valores cero durante la deserialización (conversión de JSON a objeto/estructura).

Veamos un ejemplo sencillo de este error. Tenemos la estructura Usuario, que tiene dos campos: NombreDeUsuario y Contraseña, uno es público y el otro es privado. Debido a esto, al intentar serializar, el JSON no contendrá el campo Contraseña, y al deserializar, veremos que el campo Contraseña no se llena:

package main

import (
    "encoding/json"
    "fmt"
)

type Usuario struct {
    NombreDeUsuario string `json:"nombre_de_usuario"`
    Contraseña      string `json:"contraseña"` // campo no exportado
}

func main() {
    // Caso de serialización
    jose := Usuario{NombreDeUsuario: "jose", Contraseña: "contraseña-de-jose"}
    stringJson, errMarshal := json.Marshal(jose) // El campo 'Contraseña' no se incluirá porque es privado
    if errMarshal != nil {
        fmt.Println(errMarshal.Error())
        panic(errMarshal)
    }
    fmt.Println("json de jose: " + string(stringJson))

    // Caso de deserialización
    var maria Usuario
    jsonMaria := []byte("{\"nombre_de_usuario\":\"maria\", \"contraseña\":\"contraseña-de-maria\"}")
    errUnmarshal := json.Unmarshal(jsonMaria, &maria)
    if errUnmarshal != nil {
        fmt.Println(errUnmarshal.Error())
        panic(errUnmarshal)
    }
    fmt.Println(fmt.Sprintf("estructura de maria: %+v", maria))
}
Enter fullscreen mode Exit fullscreen mode

Al ejecutar el comando go run error_no_exportado.go, podemos ver la siguiente salida en la consola:

json de jose: {"nombre_de_usuario":"jose"}
estructura de maria: {NombreDeUsuario:maria Contraseña:}
Enter fullscreen mode Exit fullscreen mode

Para evitar perder tiempo tratando de descubrir por qué no se guardó un campo específico, asegúrese siempre de que solo los campos que deben serializarse estén exportados.

package main

import (
    "encoding/json"
    "fmt"
)

type Usuario struct {
    NombreDeUsuario string `json:"nombre_de_usuario"`
    Contraseña      string `json:"contraseña"` // campo no exportado
}

func main() {
    // Caso de serialización
    jose := Usuario{NombreDeUsuario: "jose", Contraseña: "contraseña-de-jose"}
    stringJson, errMarshal := json.Marshal(jose) // El campo 'Contraseña' no se incluirá porque es privado
    if errMarshal != nil {
        fmt.Println(errMarshal.Error())
        panic(errMarshal)
    }
    fmt.Println("json de jose: " + string(stringJson))

    // Caso de deserialización
    var maria Usuario
    jsonMaria := []byte("{\"nombre_de_usuario\":\"maria\", \"contraseña\":\"contraseña-de-maria\"}")
    errUnmarshal := json.Unmarshal(jsonMaria, &maria)
    if errUnmarshal != nil {
        fmt.Println(errUnmarshal.Error())
        panic(errUnmarshal)
    }
    fmt.Println(fmt.Sprintf("estructura de maria: %+v", maria))
}
Enter fullscreen mode Exit fullscreen mode

Al ejecutar la solución, obtendremos la siguiente salida:

json de jose: {"nombre_de_usuario":"jose"}
estructura de maria: {NombreDeUsuario:maria Contraseña:}
Enter fullscreen mode Exit fullscreen mode

Error 2: No validar datos de entrada para Unmarshal()

Este segundo error es algo que vi recientemente y recordé que ya había pasado por eso hace un tiempo, en el artículo de Bahar Shah en Medium: "Los errores de Unmarshal en Go pueden no funcionar de la manera que crees". El título es bastante descriptivo y tiene mucho sentido: en **Unmarshal()**, el error que se devuelve solo se refiere a si el JSON está mal formado, no valida los campos. Entonces, si recibe cualquier JSON, incluso si no contiene ningún campo de su estructura, la deserialización se considera un éxito, es decir, sin errores, pero su objeto/estructura no contendrá ningún valor.

Por lo tanto, antes de utilizar la función **Unmarshal()**, es importante validar los datos de entrada para evitar errores provenientes del JSON. Para hacer la validación, existen algunas prácticas en Go, como crear su propia función de validación desde cero o usar el paquete Validator para simplificar la declaración y validación. Veamos un ejemplo usando Validator, ya que es una solución que se puede expandir para obtener una validación más completa.

Nuestro código recibe un JSON con información sobre un perro, ¿verdad? En esta validación, nuestro objeto tiene 3 campos: Nombre, Edad y EsAmigable, cada uno con su propia validación. Por ejemplo, Nombre es obligatorio (required) y debe tener al menos 3 caracteres y como máximo 12 caracteres (min=3,max=12), mientras que Edad es obligatoria y debe ser un número (numeric), y finalmente, EsAmigable es simplemente obligatoria. Ahora, imaginemos que por error recibimos los datos del perro, pero falta un campo. Con esta validación, evitaremos guardar datos incompletos en una base de datos, por ejemplo. Veamos el código a continuación con el error:

package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "github.com/go-playground/validator/v10"
)

type Perro struct {
    Nombre      string `json:"nombre" validate:"required,min=3,max=12"`
    Edad        int    `json:"edad" validate:"required,numeric"`
    EsAmigable  bool   `json:"es_amigable" validate:"required"`
}

type ErroresDeValidacion struct {
    Campo string
    Tag   string
    Valor string
}

var Validator = validator.New()

func validaPerro(perro Perro) []ErroresDeValidacion {
    errores := make([]ErroresDeValidacion, 0)

    err := Validator.Struct(perro)
    if err != nil {
        for _, errorValidator := range err.(validator.ValidationErrors) {
            error := ErroresDeValidacion{
                Campo: errorValidator.Field(),
                Tag:   errorValidator.Tag(),
                Valor: errorValidator.Param(),
            }
            errores = append(errores, error)
        }
        return errores
    }
    return nil
}

func main() {
    jsonPerro := []byte("{\"nombre\":\"bilu\", \"edad\":5}")
    var perro Perro
    errUnmarshal := json.Unmarshal(jsonPerro, &perro)
    if errUnmarshal != nil {
        fmt.Println(errUnmarshal.Error())
        panic(errUnmarshal)
    }

    erroresValidacion := validaPerro(perro)
    if len(erroresValidacion) > 0 {
        fmt.Println(fmt.Sprintf("errores: %+v", erroresValidacion))
        panic(errors.New("error al validar el JSON del perro"))
    }

    fmt.Println(fmt.Sprintf("perro: %+v", perro))
}
Enter fullscreen mode Exit fullscreen mode

Al ejecutar el código con go run error_no_validado.go, obtenemos la siguiente salida:

errores: [{Campo:EsAmigable Tag:required Valor:}]
panic: error al validar el JSON del perro


goroutine 1 [running]:
main.main()
        camino/a/tu/codigo/erro_nao_validando_campos.go:54
exit status 2
Enter fullscreen mode Exit fullscreen mode

Ahora, si ejecutamos el mismo código con la corrección en el JSON:

jsonPerro := []byte("{\"nombre\":\"bilu\", \"edad\":5, \"es_amigable\":true}")
Enter fullscreen mode Exit fullscreen mode

Y volvemos a ejecutar el código, la salida será la siguiente, mostrando que la validación funciona y evitará datos incorrectos:

perro: {Nombre:bilu Edad:5 EsAmigable:true}
Enter fullscreen mode Exit fullscreen mode

Error 3: ¡Bucles infinitos en referencias circulares al usar Marshal()!

Ahora, un error que puede ocurrir si tiene que lidiar con referencias circulares en el código (algo muy específico, como manipular un árbol binario) es un bucle infinito al llamar a la función **Marshal()** o **Unmarshal()**, lo que puede causar un bloqueo del programa o un consumo excesivo de recursos.

Quizás haya intentado imaginar un escenario en el que esto suceda, ¿verdad? Bien, lo simplificaré lo más posible: imagina un escenario en el que estás creando una red social y necesitas devolver los datos de María, que tiene a Juan en su lista de amigos. Por lo tanto, obtuviste los datos de María y Juan de la base de datos y ahora vas a completar su relación de amistad y luego convertir el objeto que representa a María en una serie de bytes (una matriz de bytes de Go) para devolverlo en tu API, ya que Go convertirá estos datos en una cadena de respuesta en el punto final. Si te confundiste con esta conversión de objeto a serie de bytes para devolverla en la API, no te preocupes, en otro artículo explicaré más sobre eso.

Bien, aquí tienes un ejemplo de código con el error:

package main

import (
    "encoding/json"
    "fmt"
)

type Persona struct {
    Nombre  string    `json:"nombre"`
    Amigos  []*Persona `json:"amigos"`
}

func main() {
    maria := &Persona{Nombre: "Maria"}
    juan := &Persona{Nombre: "Juan"}
    maria.Amigos = []*Persona{juan}
    juan.Amigos = []*Persona{maria}

    data, err := json.Marshal(maria) // Puede entrar en un ciclo infinito aquí
    if err != nil {
        fmt.Println(err.Error())
        panic(err)
    }
    fmt.Println(string(data))
}
Enter fullscreen mode Exit fullscreen mode

Al ejecutar el código anterior con go run rede_social_loop_infinito_marshal.go, verás que Go está preparado para detectar este ciclo y devolverá un error. La salida será la siguiente:

json: unsupported value: encountered a cycle via *main.Persona
panic: json: unsupported value: encountered a cycle via *main.Persona

goroutine 1 [running]:
main.main()
        camino/a/tu/codigo/rede_social_loop_infinito_marshal.go:22
exit status 2
Enter fullscreen mode Exit fullscreen mode

Ahora bien, para solucionar el problema que hemos generado, podemos hacerlo de dos formas. La primera es pedir que ese campo se ignore en el JSON, lo cual hacemos en la declaración del campo usando json:"-" en lugar de json:"amigos". Pero esto haría que no enviemos la lista de amigos, y queremos enviarla, ¿verdad? Entonces, resolvamos de otra manera, usando un formato intermedio para la serialización. En general, este formato será más simplificado. Aquí tienes el código:

package main

import (
    "encoding/json"
    "fmt"
)

type Persona struct {
    Nombre  string    `json:"nombre"`
    Amigos  []*Persona `json:"amigos"`
}

type PersonaSerializable struct {
    Nombre  string   `json:"nombre"`
    Amigos []string `json:"amigos"`
}

func convertirAPersonaSerializable(persona *Persona) PersonaSerializable {
    amigosSerializables := make([]string, len(persona.Amigos))

    for indice, amigo := range persona.Amigos {
        amigosSerializables[indice] = amigo.Nombre
    }

    return PersonaSerializable{
        Nombre:  persona.Nombre,
        Amigos: amigosSerializables,
    }
}

func main() {
    maria := &Persona{Nombre: "Maria"}
    juan := &Persona{Nombre: "Juan"}
    maria.Amigos = []*Persona{juan}
    juan.Amigos = []*Persona{maria}

    data, err := json.Marshal(convertirAPersonaSerializable(maria)) // ahora no habrá errores de bucle
    if err != nil {
        fmt.Println(err.Error())
        panic(err)
    }

    fmt.Println(string(data))
}
Enter fullscreen mode Exit fullscreen mode

Esta solución funciona bien cuando ejecutamos el código con go run solucion_rede_social_loop_infinito_marshal.go. Sé que es un código forzado y que podríamos evitar este problema de otra manera, pero esta es la forma más sencilla que se me ocurrió para ilustrar este error. Es puramente didáctico y espero que haya transmitido el error y cómo corregirlo.

Conclusión

Llegamos al final de este artículo. Lo traje con errores y soluciones después de comenzar a leer "100 Go Mistakes and How to Avoid Them", donde el autor del libro comienza presentando errores y malas prácticas y luego ofrece soluciones para estos casos que presenta. Así que decidí probar el mismo enfoque con algo más básico para ver cómo funcionaría. ¡Espero que les haya gustado!

En resumen, las funciones **Marshal()** y **Unmarshal()** son recursos cruciales cuando se trabaja con JSON en Go, pero requieren precauciones específicas para garantizar que su código sea eficiente, seguro y libre de errores. Al estar consciente de los peligros potenciales y seguir algunas de las prácticas que mencioné aquí, podrá utilizar estas funciones de manera efectiva y sin muchas preocupaciones.

Sin embargo, siempre recuerde que pueden (y deben) existir otros errores que no he mencionado aquí, por lo que siempre debe consultar la documentación oficial del paquete encoding/json para obtener información detallada sobre el uso correcto de estas funciones, con ejemplos, y siempre practicar y conversar con otras personas que programen en Go para aprender de ellos.

Referencias
Donovan, A. A., & Kernighan, B. W. (2016). The Go Programming Language. Novatec Editora.
Página del paquete encoding/json. Disponible en: https://pkg.go.dev/encoding/json.
Stackoverflow. Disponible en: https://stackoverflow.com/questions/32708717/go-when-will-json-unmarshal-to-struct-return-error
Artículo de Medium de Bahar Shah - "Los errores de Unmarshal en Go pueden no funcionar de la manera que crees". Disponible en: https://baharbshah.medium.com/gos-unmarshal-errors-might-not-work-the-way-you-think-they-do-949f8fe15a09
Página del paquete Validator. Disponible en: https://pkg.go.dev/github.com/go-playground/validator/v10

💖 💪 🙅 🚩
augustoasilva
Augusto Silva

Posted on September 9, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related