La magia de JSON en Go: ¿por qué unos campos son visibles y otros no?
\n¿Alguna vez se ha encontrado con una situación en la que su estructura de Go envía en la respuesta JSON algo que el cliente no necesita ver? Por ejemplo, la contraseña de un usuario, el ID interno de la base de datos o una clave secreta. O, por el contrario, necesita mostrar un campo solo bajo una condición determinada (por ejemplo, si no está vacío).
\nEste problema es uno de los más populares en Stack Overflow, y con razón. Trabajar con JSON en Go es un arte de equilibrio entre el rigor de los tipos y la flexibilidad de la representación. Hoy analizaremos todas las formas: desde la simple etiqueta json:"-" hasta soluciones personalizadas complejas con interfaces. ¡Allá vamos!
1. La forma más sencilla: la etiqueta json:"-"
\nGo permite controlar la serialización de una estructura mediante etiquetas especiales. Si desea ocultar permanentemente un campo del JSON, simplemente agregue json:"-".
type User struct {\n Name string `json:"name"`\n Password string `json:"-"`\n Age int `json:"age"`\n}\n\nfunc main() {\n u := User{Name: "Alicia", Password: "supersecret", Age: 30}\n data, _ := json.Marshal(u)\n fmt.Println(string(data))\n // Salida: {"name":"Alicia","age":30}\n}\nEl campo Password desapareció como si nunca hubiera existido. Ideal para datos confidenciales que nunca deben salir del servidor.
2. Ocultación condicional: omitempty
\nA veces es necesario ocultar un campo solo si está vacío. Para esto se utiliza la opción omitempty (abreviatura de "omit if empty").
type Product struct {\n Name string `json:"name"`\n Description string `json:"description,omitempty"`\n Price float64 `json:"price,omitempty"`\n}\n\nfunc main() {\n p1 := Product{Name: "Libro", Price: 500.0}\n data1, _ := json.Marshal(p1)\n fmt.Println(string(data1)) // {"name":"Libro","price":500}\n\n p2 := Product{Name: "Curso"}\n data2, _ := json.Marshal(p2)\n fmt.Println(string(data2)) // {"name":"Curso"}\n}\nImportante: omitempty solo funciona con valores "cero": "", 0, false, nil (para slices, maps, punteros).
Trampa con campos numéricos
\nTenga cuidado: si Price es igual a 0, también se ocultará. En algunas lógicas de negocio esto no es aceptable (un producto gratuito también es un precio). En tales casos, es mejor usar punteros.
3. Uso de punteros (*) e interfaces
\nLos punteros son su mejor amigo cuando necesita distinguir entre "valor no especificado" y "valor igual a cero".
\ntype UserProfile struct {\n Name string `json:"name"`\n Age *int `json:"age,omitempty"` // puntero a int\n Bio *string `json:"bio,omitempty"`\n}\n\nfunc main() {\n // Edad no especificada — el campo se oculta\n u1 := UserProfile{Name: "Juan"}\n data1, _ := json.Marshal(u1)\n fmt.Println(string(data1)) // {"name":"Juan"}\n\n // Edad = 0 — es un valor significativo\n zero := 0\n u2 := UserProfile{Name: "María", Age: &zero}\n data2, _ := json.Marshal(u2)\n fmt.Println(string(data2)) // {"name":"María","age":0}\n}\nEste enfoque se usa a menudo en APIs REST, donde es necesario actualizar solo los campos enviados (actualización parcial, solicitudes PATCH).
\n\n4. Serialización personalizada: la interfaz json.Marshaler
\nSi las etiquetas estándar no son suficientes, puede controlar completamente el proceso. Implemente la interfaz json.Marshaler y escriba su propia lógica.
type DynamicUser struct {\n ID int\n Name string\n ShowEmail bool\n Email string\n}\n\nfunc (u DynamicUser) MarshalJSON() ([]byte, error) {\n // Creamos una estructura anónima solo para la serialización\n m := map[string]interface{}{\n "id": u.ID,\n "name": u.Name,\n }\n if u.ShowEmail {