Serialización con encoding/json: Marshal y Unmarshal

La serialización es el proceso de transformar una estructura de datos de Go a un formato de transporte, como JSON, y viceversa. En Go, esto se logra principalmente mediante json.Marshal, que convierte un valor en un slice de bytes ([]byte), y json.Unmarshal, que reconstruye un valor a partir de esos bytes.

Este proceso funciona gracias a la reflexión (reflect), una capacidad del lenguaje para inspeccionar tipos y valores en tiempo de ejecución. Para que el paquete encoding/json pueda acceder a los campos de un struct, estos deben estar exportados; es decir, deben comenzar con una letra mayúscula. Si un campo es privado (minúscula), el codificador simplemente lo ignorará porque no tiene permisos para acceder a él desde fuera del paquete.

Debes usar Marshal cuando necesites enviar datos a través de una API o guardarlos en un archivo, y Unmarshal cuando recibas datos de una fuente externa. Si el output es para depuración o logs manuales, MarshalIndent te dará un formato mucho más legible gracias a la sangría. Si intentas usar Unmarshal pasando un valor en lugar de un puntero, el proceso fallará porque el runtime no podrá escribir los datos en la dirección de memoria del objeto.

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

// Usuario define la estructura de nuestros datos.
// Usamos struct tags (ej. `json:"id"`) para mapear nombres de campos
// de Go (Mayúsculas) a nombres de JSON (minúsculas/estándar).
type Usuario struct {
	ID        int      `json:"id"`
	Nombre    string   `json:"nombre"`
	Roles     []string `json:"roles"`     // Slice que puede ser nil
	Etiquetas []string `json:"etiquetas"` // Slice inicializado como vacío
	secret    string   `json:"secret"`    // Campo no exportado: será ignorado
}

func main() {
	// 1. Preparar un objeto con diferentes estados de slices para observar la salida.
	u := Usuario{
		ID:        101,
		Nombre:    "marcos_dev",
		Roles:     nil,             // Se serializará como "null"
		Etiquetas: []string{},      // Se serializará como "[]"
		secret:    "top_secret",    // No aparecerá en el JSON
	}

	// MarshalIndent genera un JSON con sangría (dos espacios) para legibilidad.
	data, err := json.MarshalIndent(u, "", "  ")
	if err != nil {
		log.Fatalf("Error al serializar: %v", err)
	}
	fmt.Printf("JSON generado (MarshalIndent):\n%s\n\n", string(data))

	// 2. Deserializar (Unmarshal) un JSON de vuelta a una estructura.
	// El JSON incluye un campo "extra" que no existe en nuestro struct; 
	// Unmarshal lo ignorará por defecto sin dar error.
	jsonInput := []byte(`{"id":202,"nombre":"alicia","roles":["admin","user"],"etiquetas":[],"extra":"ignorado"}`)
	var u2 Usuario

	// Es crucial pasar la dirección de memoria (&u2) para que Unmarshal pueda
	// modificar el valor de la variable declarada.
	if err := json.Unmarshal(jsonInput, &u2); err != nil {
		log.Fatalf("Error al deserializar: %v", err)
	}

	fmt.Printf("Estructura recuperada (Unmarshal):\n%+v\n", u2)
}

Desglose del ejemplo

En el código anterior, hemos configurado el struct Usuario con struct tags como `json:"id"`. Esto es fundamental porque en Go las convenciones dictan que los campos deben ser mayúsculos para ser públicos, pero en JSON lo estándar es usar snake_case o camelCase.

Cuando ejecutamos json.MarshalIndent(u, "", " "), el paquete utiliza reflexión para recorrer los campos de u. Al encontrar secret, nota que es minúscula y lo omite. Al encontrar Roles siendo nil, escribe null. Sin embargo, con Etiquetas, que es un slice vacío []string{}, escribe []. Esta distinción es sutil pero crítica.

En la segunda parte, json.Unmarshal(jsonInput, &u2), pasamos &u2 porque Unmarshal necesita un puntero para escribir los datos directamente en la memoria de la variable u2. Si pasáramos u2 por valor, recibiríamos un error en tiempo de ejecución indicando que el destino no es un puntero. Notarás que el campo extra en el JSON es ignorado; esta es una característica de diseño de encoding/json para permitir la compatibilidad hacia adelante cuando las APIs evolucionan.

El error frecuente

Un error sutil pero común ocurre al manejar la distinción entre un slice nil y un slice vacío []T{} durante la serialización.

// Caso A: Slice nil
var rolesA []string
// Marshal produce: "roles": null

// Caso B: Slice vacío
rolesB := []string{}
// Marshal produce: "roles": []

Si estás desarrollando una API que será consumida por un cliente de TypeScript o algún framework de frontend estricto, recibir un null en lugar de un array [] puede causar errores de ejecución (como cannot read property 'map' of null). Si tu contrato de API exige que el campo sea siempre un array, asegúrate de inicializar tus slices como make([]T, 0) o []T{} en lugar de dejarlos como nil.

118

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio