JSON Formatter Go — Guía de MarshalIndent()
Usa el Formateador y Embellecedor JSON gratuito directamente en tu navegador — sin instalación.
Probar Formateador y Embellecedor JSON online →Cuando trabajo en un microservicio Go y necesito inspeccionar una respuesta de API o un archivo de configuración, el JSON compacto es el primer obstáculo — una sola línea con cientos de campos anidados no me dice casi nada de un vistazo. Para formatear JSON en Go, la biblioteca estándar te da todo lo que necesitas: json.MarshalIndent está integrado en encoding/json, se incluye con cada instalación de Go y no requiere dependencias externas. Esta guía cubre el panorama completo: struct tags, implementaciones personalizadas de MarshalJSON() , json.Indent para reformatear bytes en bruto, streaming de archivos grandes con json.Decoder, y cuándo recurrir a go-json para rutas de alto rendimiento, además de comandos de una sola línea para formatear rápidamente en la terminal. Todos los ejemplos usan Go 1.21+.
- ✓json.MarshalIndent(v, "", "\t") es de la biblioteca estándar — cero dependencias, incluido en cada instalación de Go.
- ✓Los struct tags json:"field_name,omitempty" controlan las claves de serialización y omiten los campos con valor cero.
- ✓Implementa MarshalJSON() en cualquier tipo para controlar completamente su representación JSON.
- ✓json.Indent() reformatea []byte ya serializados sin volver a parsear la struct — más rápido para bytes en bruto.
- ✓Para archivos grandes (>100 MB): usa json.Decoder con Token() para hacer streaming sin cargar todo en memoria.
- ✓go-json es un reemplazo directo 3–5× más rápido que encoding/json para APIs de alto rendimiento.
¿Qué es el formateo de JSON?
El formateo de JSON — también llamado pretty-printing — transforma una cadena JSON compacta y minificada en un layout legible con indentación y saltos de línea consistentes. Los datos subyacentes son idénticos; solo cambian los espacios en blanco. El JSON compacto es óptimo para la transferencia de red donde cada byte importa; el JSON formateado es óptimo para depuración, revisión de código, inspección de logs y edición de archivos de configuración. El paquete encoding/json de Go gestiona ambos modos con una sola llamada a función — alterna entre salida compacta e indentada eligiendo entre json.Marshal y json.MarshalIndent.
{"service":"payments","port":8443,"workers":4}{
"service": "payments",
"port": 8443,
"workers": 4
}json.MarshalIndent() — El enfoque de la biblioteca estándar
json.MarshalIndent vive en el paquete encoding/json , que forma parte de la biblioteca estándar de Go — no requiere go get. Su firma es MarshalIndent(v any, prefix, indent string) ([]byte, error): la cadena prefix se antepone a cada línea de salida (casi siempre se deja vacía), e indent se repite una vez por nivel de anidamiento. Pasa "\t" para tabulaciones o " " para dos espacios.
package main
import (
"encoding/json"
"fmt"
"log"
)
type ServiceConfig struct {
Host string `json:"host"`
Port int `json:"port"`
Workers int `json:"workers"`
TLSEnabled bool `json:"tls_enabled"`
AllowedIPs []string `json:"allowed_ips"`
}
func main() {
cfg := ServiceConfig{
Host: "payments-api.internal",
Port: 8443,
Workers: 8,
TLSEnabled: true,
AllowedIPs: []string{"10.0.0.0/8", "172.16.0.0/12"},
}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
log.Fatalf("marshal config: %v", err)
}
fmt.Println(string(data))
}
// {
// "host": "payments-api.internal",
// "port": 8443,
// "workers": 8,
// "tls_enabled": true,
// "allowed_ips": [
// "10.0.0.0/8",
// "172.16.0.0/12"
// ]
// }La elección entre tabulaciones y espacios es principalmente una convención de equipo. Muchos proyectos Go prefieren las tabulaciones porque gofmt (que formatea el código fuente Go) las utiliza. La indentación de dos o cuatro espacios es habitual cuando el JSON va destinado a un consumidor de JavaScript o Python. Aquí está la misma struct con ambos estilos de indentación en paralelo:
// Indentación con tabulaciones — preferida en las herramientas nativas de Go
data, _ := json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }
// Indentación de dos espacios — habitual para APIs con consumidores JS/Python
data, _ = json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }
// Indentación de cuatro espacios
data, _ = json.MarshalIndent(cfg, "", " ")
// {
// "host": "payments-api.internal",
// "port": 8443
// }json.Marshal(v) cuando necesites salida compacta — payloads de red, valores en caché o cualquier ruta donde el tamaño en bytes importa. Acepta el mismo argumento de valor y tiene la misma semántica de error, pero produce JSON en una sola línea sin espacios en blanco.Struct Tags — Controlar los nombres de campo y omitempty
Los struct tags de Go son literales de cadena colocados tras las declaraciones de campo que indican a encoding/jsoncómo serializar cada campo. Hay tres directivas clave: json:"name" renombra el campo en la salida, omitempty omite el campo cuando contiene el valor cero de su tipo, y json:"-" excluye el campo por completo — útil para contraseñas, identificadores internos o campos que nunca deben cruzar el límite del servicio.
type UserProfile struct {
ID string `json:"id"`
Email string `json:"email"`
DisplayName string `json:"display_name,omitempty"` // omitir si cadena vacía
AvatarURL *string `json:"avatar_url,omitempty"` // omitir si puntero nil
IsAdmin bool `json:"is_admin,omitempty"` // omitir si false
passwordHash string // no exportado — excluido automáticamente
}
// Usuario con todos los campos opcionales completados
full := UserProfile{
ID: "usr_7b3c", Email: "c.mendoza@ejemplo.com",
DisplayName: "Carlos Mendoza", IsAdmin: true,
}
// {
// "id": "usr_7b3c",
// "email": "c.mendoza@ejemplo.com",
// "display_name": "Carlos Mendoza",
// "is_admin": true
// }
// Usuario sin campos opcionales — se omiten por completo
minimal := UserProfile{ID: "usr_2a91", Email: "a.torres@ejemplo.com"}
// {
// "id": "usr_2a91",
// "email": "a.torres@ejemplo.com"
// }El tag json:"-" es la opción correcta para campos que deben excluirse incondicionalmente independientemente de su valor — típicamente secretos, campos de seguimiento interno o datos correctos en memoria que nunca deben serializarse hacia ningún sistema externo.
type AuthToken struct {
TokenID string `json:"token_id"`
Subject string `json:"sub"`
IssuedAt int64 `json:"iat"`
ExpiresAt int64 `json:"exp"`
SigningKey []byte `json:"-"` // nunca serializado
RefreshToken string `json:"-"` // nunca serializado
}
tok := AuthToken{
TokenID: "tok_8f2a", Subject: "usr_7b3c",
IssuedAt: 1741614120, ExpiresAt: 1741617720,
SigningKey: []byte("secreto"), RefreshToken: "rt_9e4f",
}
data, _ := json.MarshalIndent(tok, "", " ")
// {
// "token_id": "tok_8f2a",
// "sub": "usr_7b3c",
// "iat": 1741614120,
// "exp": 1741617720
// }
// SigningKey y RefreshToken nunca aparecenencoding/json independientemente de cualquier tag. No necesitas añadir json:"-" a los campos no exportados — la exclusión es automática y no puede anularse.MarshalJSON() personalizado — Manejo de tipos no estándar
Cualquier tipo Go puede implementar la interfaz json.Marshaler definiendo un método MarshalJSON() ([]byte, error). Cuando encoding/json encuentra dicho tipo, llama al método en lugar de su serialización basada en reflexión por defecto. Este es el patrón Go canónico para tipos de dominio que necesitan una representación de red específica — valores monetarios, enums de estado, formatos de fecha personalizados o cualquier tipo que almacene datos de forma diferente a cómo debe serializarse.
Tipo personalizado — Money con conversión de céntimos a decimal
package main
import (
"encoding/json"
"fmt"
"log"
)
type Money struct {
Amount int64 // almacenado en céntimos para evitar errores de punto flotante
Currency string
}
func (m Money) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Amount float64 `json:"amount"`
Currency string `json:"currency"`
Display string `json:"display"`
}{
Amount: float64(m.Amount) / 100,
Currency: m.Currency,
Display: fmt.Sprintf("%s %.2f", m.Currency, float64(m.Amount)/100),
})
}
type Invoice struct {
ID string `json:"id"`
Subtotal Money `json:"subtotal"`
Tax Money `json:"tax"`
Total Money `json:"total"`
}
func main() {
inv := Invoice{
ID: "inv_9a2f91bc",
Subtotal: Money{Amount: 19900, Currency: "EUR"},
Tax: Money{Amount: 1592, Currency: "EUR"},
Total: Money{Amount: 21492, Currency: "EUR"},
}
data, err := json.MarshalIndent(inv, "", " ")
if err != nil {
log.Fatalf("marshal invoice: %v", err)
}
fmt.Println(string(data))
}
// {
// "id": "inv_9a2f91bc",
// "subtotal": { "amount": 199, "currency": "EUR", "display": "EUR 199.00" },
// "tax": { "amount": 15.92, "currency": "EUR", "display": "EUR 15.92" },
// "total": { "amount": 214.92, "currency": "EUR", "display": "EUR 214.92" }
// }Enum de estado — Representación como cadena
type OrderStatus int
const (
StatusPending OrderStatus = iota
StatusPaid
StatusShipped
StatusCancelled
)
var orderStatusNames = map[OrderStatus]string{
StatusPending: "pendiente",
StatusPaid: "pagado",
StatusShipped: "enviado",
StatusCancelled: "cancelado",
}
func (s OrderStatus) MarshalJSON() ([]byte, error) {
name, ok := orderStatusNames[s]
if !ok {
return nil, fmt.Errorf("estado de pedido desconocido: %d", s)
}
return json.Marshal(name)
}
type Order struct {
ID string `json:"id"`
Status OrderStatus `json:"status"`
}
o := Order{ID: "ord_3c7f", Status: StatusShipped}
data, _ := json.MarshalIndent(o, "", " ")
// {
// "id": "ord_3c7f",
// "status": "enviado"
// }MarshalJSON() y UnmarshalJSON() juntos. Si solo implementas la serialización, hacer un ciclo completo del tipo por JSON (serializar → almacenar → deserializar) perderá estructura silenciosamente o devolverá el tipo incorrecto. El par forma un contrato que garantiza que el tipo puede sobrevivir un ciclo JSON completo.UUID — Serializar como cadena
La biblioteca estándar de Go no tiene tipo UUID. La elección más habitual es github.com/google/uuid, que ya implementa MarshalJSON() y serializa como una cadena RFC 4122 entre comillas. Si usas un [16]byte en bruto o un tipo ID personalizado, implementa la interfaz tú mismo para evitar blobs binarios codificados en base64 en tu salida JSON.
import (
"encoding/json"
"fmt"
"github.com/google/uuid"
)
type AuditEvent struct {
EventID uuid.UUID `json:"event_id"` // serializa como "550e8400-e29b-41d4-a716-446655440000"
SessionID uuid.UUID `json:"session_id"`
Action string `json:"action"`
ActorID string `json:"actor_id"`
OccuredAt string `json:"occurred_at"`
}
event := AuditEvent{
EventID: uuid.New(),
SessionID: uuid.MustParse("550e8400-e29b-41d4-a716-446655440000"),
Action: "usuario.contraseña_cambiada",
ActorID: "usr_7f3a91bc",
OccuredAt: "2026-03-10T14:22:00Z",
}
data, _ := json.MarshalIndent(event, "", " ")
fmt.Println(string(data))
// {
// "event_id": "a4b2c1d0-...",
// "session_id": "550e8400-e29b-41d4-a716-446655440000",
// "action": "usuario.contraseña_cambiada",
// "actor_id": "usr_7f3a91bc",
// "occurred_at": "2026-03-10T14:22:00Z"
// }[16]byte sin un tipo personalizado, encoding/json los codifica como cadena base64 — p. ej. "VQ6EAOKbQdSnFkRmVUQAAA==". Usa siempre un tipo UUID adecuado o implementa MarshalJSON() para emitir el formato canónico de cadena con guiones.Referencia de parámetros de json.MarshalIndent()
La firma de la función es func MarshalIndent(v any, prefix, indent string) ([]byte, error). Tanto prefix como indent son literales de cadena — no hay atajo numérico como el indent=4 de Python.
Opciones comunes de struct tags:
json.Indent() — Reformatear bytes JSON existentes
Cuando ya tienes un []byte de JSON — por ejemplo del cuerpo de una respuesta HTTP, una columna jsonb de Postgres, o un archivo leído con os.ReadFile — no necesitas definir una struct ni deserializar antes de poder formatear. json.Indent reformatea directamente los bytes en bruto escribiendo la salida indentada en un bytes.Buffer.
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
)
func main() {
// Simulando un payload JSON en bruto de un servicio externo
raw := []byte(`{"trace_id":"tr_9a2f","service":"checkout","latency_ms":342,"error":null}`)
var buf bytes.Buffer
if err := json.Indent(&buf, raw, "", " "); err != nil {
log.Fatalf("indent: %v", err)
}
fmt.Println(buf.String())
}
// {
// "trace_id": "tr_9a2f",
// "service": "checkout",
// "latency_ms": 342,
// "error": null
// }Un patrón que uso habitualmente en microservicios es llamar a json.Indent antes de escribir en logs estructurados — añade una sobrecarga mínima y hace las entradas de log mucho más fáciles de leer durante un incidente. La función es especialmente útil para registrar respuestas HTTP, formatear cadenas JSON almacenadas y pipelines de formato-al-leer donde la definición de la struct no está disponible.
func logResponse(logger *slog.Logger, statusCode int, body []byte) {
var pretty bytes.Buffer
if err := json.Indent(&pretty, body, "", " "); err != nil {
// El cuerpo no es JSON válido — registrar en bruto
logger.Debug("respuesta del upstream", "status", statusCode, "body", string(body))
return
}
logger.Debug("respuesta del upstream", "status", statusCode, "body", pretty.String())
}json.Indent NO valida completamente el JSON más allá de lo estructuralmente necesario para insertar espacios en blanco. Para una validación sintáctica completa, llama a json.Valid(data) primero y gestiona el caso false antes de intentar la indentación.Formatear JSON de un archivo y una respuesta HTTP
Dos de los escenarios más comunes en servicios Go son formatear JSON leído de un archivo en disco (archivos de configuración, datos de fixtures, semillas de migración) y formatear cuerpos de respuesta HTTP para logging de depuración o aserciones en tests. Ambos siguen el mismo patrón: leer bytes, llamar a json.Indent o deserializar y luego json.MarshalIndent, escribir de vuelta o registrar.
Leer archivo → Formatear → Escribir de vuelta
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"os"
)
func formatJSONFile(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("leer %s: %w", path, err)
}
if !json.Valid(data) {
return fmt.Errorf("JSON inválido en %s", path)
}
var buf bytes.Buffer
if err := json.Indent(&buf, data, "", " "); err != nil {
return fmt.Errorf("indent %s: %w", path, err)
}
if err := os.WriteFile(path, buf.Bytes(), 0644); err != nil {
return fmt.Errorf("escribir %s: %w", path, err)
}
return nil
}
func main() {
if err := formatJSONFile("config/database.json"); err != nil {
log.Fatalf("formatear config: %v", err)
}
fmt.Println("config/database.json formateado correctamente")
}Respuesta HTTP → Decodificar → Formatear para logging de depuración
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
type HealthResponse struct {
Status string `json:"status"`
Version string `json:"version"`
Checks map[string]string `json:"checks"`
UptimeSec int64 `json:"uptime_seconds"`
}
func main() {
resp, err := http.Get("https://api.payments.internal/v2/health")
if err != nil {
log.Fatalf("health check: %v", err)
}
defer resp.Body.Close()
var result HealthResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
log.Fatalf("decodificar respuesta health: %v", err)
}
pretty, err := json.MarshalIndent(result, "", " ")
if err != nil {
log.Fatalf("marshal respuesta health: %v", err)
}
fmt.Printf("Health check (%d):
%s
", resp.StatusCode, string(pretty))
}
// Health check (200):
// {
// "status": "ok",
// "version": "1.4.2",
// "checks": {
// "database": "ok",
// "cache": "ok",
// "queue": "degraded"
// },
// "uptime_seconds": 172800
// }Cuerpo de respuesta en bruto → json.Indent (sin struct necesaria)
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
)
func main() {
resp, err := http.Get("https://api.payments.internal/v2/health")
if err != nil {
log.Fatalf("request: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalf("leer cuerpo: %v", err)
}
var buf bytes.Buffer
if err := json.Indent(&buf, body, "", " "); err != nil {
log.Fatalf("indent: %v", err)
}
fmt.Println(buf.String())
}Imprimir JSON formateado de una respuesta HTTP en Go
Los dos enfoques anteriores cubren los casos más comunes: decodificar en una struct tipada y luego llamar a json.MarshalIndent (mejor cuando necesitas validar o inspeccionar campos), o leer los bytes del cuerpo en bruto con io.ReadAll y llamar directamente a json.Indent (mejor para logging de depuración rápida cuando no tienes una definición de struct a mano). El enfoque de bytes en bruto es más simple pero no te da seguridad de tipos Go ni acceso a campos — es puramente una transformación de visualización. Ambos enfoques gestionan cuerpos de respuesta grandes correctamente siempre que el cuerpo completo quepa en memoria.
// Patrón A: decodificación tipada → MarshalIndent // Usar cuando necesitas inspeccionar o validar campos específicos var result map[string]any json.NewDecoder(resp.Body).Decode(&result) pretty, _ := json.MarshalIndent(result, "", " ") fmt.Println(string(pretty)) // Patrón B: bytes en bruto → json.Indent // Usar para logging de depuración rápida — sin definición de struct necesaria body, _ := io.ReadAll(resp.Body) var buf bytes.Buffer json.Indent(&buf, body, "", " ") fmt.Println(buf.String())
Formateo de JSON en línea de comandos en proyectos Go
A veces necesitas formatear un payload JSON directamente en la terminal sin escribir un programa Go. Estos comandos de una línea son los que tengo en la memoria muscular durante el desarrollo y la respuesta a incidentes.
echo '{"service":"payments","port":8443,"workers":4}' | python3 -m json.tool
# {
# "service": "payments",
# "port": 8443,
# "workers": 4
# }# Solo formatear cat api-response.json | jq . # Extraer un campo anidado cat api-response.json | jq '.checks.database' # Filtrar un array cat audit-log.json | jq '.[] | select(.severity == "error")'
# main.go: lee stdin, formatea, escribe stdout
cat <<'EOF' > /tmp/fmt.go
package main
import ("bytes";"encoding/json";"io";"os")
func main() {
b,_:=io.ReadAll(os.Stdin)
var buf bytes.Buffer
json.Indent(&buf,b,""," ")
os.Stdout.Write(buf.Bytes())
}
EOF
echo '{"port":8080,"debug":true}' | go run /tmp/fmt.gogofmt formatea código fuente Go, no JSON. No pases archivos JSON por gofmt — producirá un error o una salida irreconocible. Usa jq . o python3 -m json.tool para archivos JSON.Alternativa de alto rendimiento — go-json
Para la gran mayoría de servicios Go, encoding/json es suficientemente rápido. Pero si la serialización JSON aparece en tu profiler — habitual en APIs REST de alto rendimiento o servicios que emiten líneas de log estructuradas grandes en cada petición — la librería go-json es un reemplazo directo que es 3–5× más rápido con una superficie de API idéntica.
go get github.com/goccy/go-json
package main
import (
// Reemplaza esto:
// "encoding/json"
// Con esto — API idéntica, sin otros cambios de código:
json "github.com/goccy/go-json"
"fmt"
"log"
)
type AuditEvent struct {
RequestID string `json:"request_id"`
UserID string `json:"user_id"`
Action string `json:"action"`
ResourceID string `json:"resource_id"`
IPAddress string `json:"ip_address"`
DurationMs int `json:"duration_ms"`
}
func main() {
event := AuditEvent{
RequestID: "req_7d2e91", UserID: "usr_4421",
Action: "factura.descarga", ResourceID: "inv_9a2f",
IPAddress: "203.0.113.45", DurationMs: 23,
}
data, err := json.MarshalIndent(event, "", " ")
if err != nil {
log.Fatalf("marshal: %v", err)
}
fmt.Println(string(data))
// La salida es idéntica a encoding/json — solo difiere la velocidad
}github.com/bytedance/sonic es la librería JSON más rápida disponible para Go, pero solo funciona en amd64 y arm64 (usa compilación JIT). Usa go-json cuando necesitas un reemplazo portable; recurre a sonic cuando estás en una arquitectura conocida y necesitas cada microsegundo en una ruta crítica.
Trabajo con archivos JSON grandes
Tanto json.MarshalIndent como json.Indent requieren que el payload completo esté en memoria. Para archivos de más de 100 MB — exportaciones de datos, logs de auditoría, payloads de consumidores Kafka — usa json.Decoder para hacer streaming del input y procesar registros de uno en uno.
Streaming de un array JSON grande con json.Decoder
package main
import (
"encoding/json"
"fmt"
"log"
"os"
)
type AuditEvent struct {
RequestID string `json:"request_id"`
UserID string `json:"user_id"`
Action string `json:"action"`
Severity string `json:"severity"`
DurationMs int `json:"duration_ms"`
}
func processAuditLog(path string) error {
file, err := os.Open(path)
if err != nil {
return fmt.Errorf("abrir %s: %w", path, err)
}
defer file.Close()
dec := json.NewDecoder(file)
// Leer el '[' de apertura
if _, err := dec.Token(); err != nil {
return fmt.Errorf("leer token de apertura: %w", err)
}
var processed int
for dec.More() {
var event AuditEvent
if err := dec.Decode(&event); err != nil {
return fmt.Errorf("decodificar evento %d: %w", processed, err)
}
// Procesar un evento a la vez — uso de memoria constante
if event.Severity == "error" {
fmt.Printf("[ERROR] %s: %s (%dms)
", event.UserID, event.Action, event.DurationMs)
}
processed++
}
fmt.Printf("Procesados %d eventos de auditoría
", processed)
return nil
}
func main() {
if err := processAuditLog("audit-2026-03.json"); err != nil {
log.Fatalf("procesar log de auditoría: %v", err)
}
}NDJSON — Un objeto JSON por línea
package main
import (
"bufio"
"encoding/json"
"fmt"
"log"
"os"
)
type LogLine struct {
Timestamp string `json:"ts"`
Level string `json:"level"`
Service string `json:"service"`
Message string `json:"msg"`
TraceID string `json:"trace_id"`
DurationMs int `json:"duration_ms,omitempty"`
}
func main() {
file, err := os.Open("service-2026-03.ndjson")
if err != nil {
log.Fatalf("abrir log: %v", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024) // 1 MB por línea
for scanner.Scan() {
var line LogLine
if err := json.Unmarshal(scanner.Bytes(), &line); err != nil {
continue // omitir líneas malformadas
}
if line.Level == "error" {
fmt.Printf("%s [%s] %s trace=%s
",
line.Timestamp, line.Service, line.Message, line.TraceID)
}
}
if err := scanner.Err(); err != nil {
log.Fatalf("scan: %v", err)
}
}os.ReadFile asignará ese buffer completo en el heap, generará presión sobre el GC y puede causar OOM en contenedores con memoria limitada.Errores comunes
Problema: Descartar el error con _ significa que un valor no serializable (un canal, una función, un número complejo) produce silenciosamente nil como salida o provoca un panic más adelante al llamar a string(nil).
Solución: Comprueba siempre el error. Si serializas un tipo que siempre debería tener éxito, un log.Fatalf con panic es mejor que la pérdida silenciosa de datos.
data, _ := json.MarshalIndent(payload, "", " ") fmt.Println(string(data)) // cadena vacía si marshal falló
data, err := json.MarshalIndent(payload, "", " ")
if err != nil {
log.Fatalf("marshal payload: %v", err)
}
fmt.Println(string(data))Problema: fmt.Println(string(data)) añade un carácter de nueva línea al final del JSON, lo que corrompe pipelines que tratan la salida como bytes en bruto — por ejemplo, al pasar a jq o escribir en un protocolo binario.
Solución: Usa os.Stdout.Write(data) para salida binaria limpia. Si necesitas una nueva línea al final para visualización humana, añádela explícitamente.
data, _ := json.MarshalIndent(cfg, "", " ") fmt.Println(string(data)) // añade una nueva línea extra al final
data, _ := json.MarshalIndent(cfg, "", " ")
os.Stdout.Write(data)
os.Stdout.Write([]byte("
")) // nueva línea explícita solo cuando sea necesarioProblema: Sin omitempty, un puntero nil *string o *int se serializa como "field": null. Esto expone nombres de campos internos y puede romper validadores de esquema JSON estrictos en el lado del consumidor.
Solución: Añade omitempty a los campos puntero que quieres que estén ausentes (no null) en la salida. Un *T nil con omitempty no produce ninguna clave en el JSON.
type WebhookPayload struct {
EventID string `json:"event_id"`
ErrorMsg *string `json:"error_msg"` // aparece como null cuando es nil
}
// {"event_id":"evt_3c7f","error_msg":null}type WebhookPayload struct {
EventID string `json:"event_id"`
ErrorMsg *string `json:"error_msg,omitempty"` // omitido cuando es nil
}
// {"event_id":"evt_3c7f"}Problema: Deserializar en map[string]any pierde la información de tipos, requiere type assertions manuales y produce un orden de claves no determinista — lo que dificulta los diffs de JSON y la comparación de logs.
Solución: Define una struct con los json tags apropiados. Las structs son seguras en cuanto a tipos, serializan más rápido, producen un orden de campos determinista que coincide con la definición de la struct y hacen el código autodocumentado.
var result map[string]any json.Unmarshal(body, &result) port := result["port"].(float64) // type assertion necesaria, provoca panic si el tipo es incorrecto
type ServiceStatus struct {
Service string `json:"service"`
Port int `json:"port"`
Healthy bool `json:"healthy"`
}
var result ServiceStatus
json.Unmarshal(body, &result)
port := result.Port // tipado, seguro, rápidoencoding/json vs alternativas — Comparación rápida
Usa json.MarshalIndent en cualquier caso donde controles la definición de la struct y necesites salida formateada — archivos de configuración, logging de depuración, fixtures de tests y logging de respuestas de API. Usa json.Indent cuando ya tienes bytes en bruto y solo necesitas añadir espacios en blanco sin una ida y vuelta por tipos Go. Cambia a go-json o sonic solo después de que el profiling confirme que la serialización JSON es un cuello de botella medible — para la mayoría de servicios, la biblioteca estándar es más que suficiente.
Preguntas frecuentes
¿Cómo imprimo JSON formateado en Go?
Llama a json.MarshalIndent(v, "", "\t") del paquete encoding/json — el segundo argumento es un prefijo por línea (normalmente vacío) y el tercero es la indentación por nivel. Pasa "\t" para tabulaciones o " " para dos espacios. No se necesita ninguna biblioteca externa; encoding/json viene incluida en la biblioteca estándar de Go.
package main
import (
"encoding/json"
"fmt"
"log"
)
func main() {
config := map[string]any{
"service": "payments-api",
"port": 8443,
"region": "eu-west-1",
}
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
log.Fatalf("marshal: %v", err)
}
fmt.Println(string(data))
// {
// "port": 8443,
// "region": "eu-west-1",
// "service": "payments-api"
// }
}¿Cuál es la diferencia entre json.Marshal y json.MarshalIndent?
json.Marshal produce JSON compacto en una sola línea sin espacios en blanco — ideal para transferencia de red donde cada byte importa. json.MarshalIndent acepta dos parámetros extra (prefix e indent) y produce una salida indentada y legible. Ambas funciones aceptan los mismos tipos de valor y devuelven ([]byte, error). El único coste de MarshalIndent es una cantidad de bytes ligeramente mayor y un mínimo de CPU adicional para insertar los espacios.
import "encoding/json"
type HealthCheck struct {
Status string `json:"status"`
Version string `json:"version"`
Uptime int `json:"uptime_seconds"`
}
h := HealthCheck{Status: "ok", Version: "1.4.2", Uptime: 86400}
compact, _ := json.Marshal(h)
// {"status":"ok","version":"1.4.2","uptime_seconds":86400}
pretty, _ := json.MarshalIndent(h, "", " ")
// {
// "status": "ok",
// "version": "1.4.2",
// "uptime_seconds": 86400
// }¿Cómo formateo un []byte de JSON sin deserializar la struct?
Usa json.Indent(&buf, src, "", "\t"). Esta función toma un []byte de JSON existente y escribe la versión indentada en un bytes.Buffer — sin definir una struct, sin type assertion y sin ida y vuelta por tipos Go. Es la opción más rápida cuando ya tienes bytes JSON en bruto, por ejemplo del cuerpo de una respuesta HTTP o de una columna de base de datos.
import (
"bytes"
"encoding/json"
"fmt"
"log"
)
raw := []byte(`{"endpoint":"/api/v2/facturas","page":1,"per_page":50,"total":312}`)
var buf bytes.Buffer
if err := json.Indent(&buf, raw, "", " "); err != nil {
log.Fatalf("indent: %v", err)
}
fmt.Println(buf.String())
// {
// "endpoint": "/api/v2/facturas",
// "page": 1,
// "per_page": 50,
// "total": 312
// }¿Por qué json.MarshalIndent devuelve un error?
encoding/json devuelve un error cuando el valor no puede representarse como JSON. Las causas más frecuentes son: serializar un canal, una función o un número complejo (no tienen equivalente JSON); una struct que implementa MarshalJSON() y devuelve un error; o un map con claves que no son string. Es importante destacar que serializar una struct con un campo no exportado o un puntero nil NO provoca un error — simplemente se omiten.
import (
"encoding/json"
"fmt"
)
// Esto devolverá un error — los canales no son serializables como JSON
ch := make(chan int)
_, err := json.MarshalIndent(ch, "", " ")
fmt.Println(err)
// json: unsupported type: chan int
// Esto está bien — los campos puntero nil con omitempty se omiten silenciosamente
type Profile struct {
ID string `json:"id"`
Avatar *string `json:"avatar,omitempty"`
}
p := Profile{ID: "usr_7b3c"}
data, _ := json.MarshalIndent(p, "", " ")
// {"id": "usr_7b3c"} — avatar omitido, sin error¿Cómo excluyo un campo de la salida JSON en Go?
Hay tres formas. Primera, usa el struct tag json:"-" — el campo siempre se excluye independientemente de su valor. Segunda, usa omitempty — el campo se excluye solo cuando contiene el valor cero de su tipo (puntero nil, cadena vacía, 0, false). Tercera, los campos no exportados (en minúsculas) son excluidos automáticamente por encoding/json sin necesidad de ningún tag.
type PaymentMethod struct {
ID string `json:"id"`
Last4 string `json:"last4"`
ExpiryMonth int `json:"expiry_month"`
ExpiryYear int `json:"expiry_year"`
CVV string `json:"-"` // siempre excluido
BillingName string `json:"billing_name,omitempty"` // excluido si está vacío
internalRef string // no exportado — excluido automáticamente
}
pm := PaymentMethod{
ID: "pm_9f3a", Last4: "4242",
ExpiryMonth: 12, ExpiryYear: 2028,
CVV: "123", internalRef: "stripe:pm_9f3a",
}
data, _ := json.MarshalIndent(pm, "", " ")
// CVV, BillingName (vacío) e internalRef no aparecen en la salida¿Cómo gestiono time.Time al serializar JSON?
encoding/json serializa time.Time en formato RFC3339Nano por defecto (p. ej. "2026-03-10T14:22:00Z"), compatible con ISO 8601. Si necesitas un formato diferente — como enteros Unix epoch para una API heredada, o una cadena solo de fecha — implementa MarshalJSON() en un tipo wrapper que embeba time.Time y devuelva el formato que necesites.
import (
"encoding/json"
"fmt"
"time"
)
// Comportamiento por defecto — RFC3339Nano, sin código personalizado
type AuditEvent struct {
Action string `json:"action"`
OccurredAt time.Time `json:"occurred_at"`
}
e := AuditEvent{
Action: "factura.pagada",
OccurredAt: time.Date(2026, 3, 10, 14, 22, 0, 0, time.UTC),
}
data, _ := json.MarshalIndent(e, "", " ")
// {
// "action": "factura.pagada",
// "occurred_at": "2026-03-10T14:22:00Z"
// }
// Personalizado: timestamp Unix como entero
type UnixTime struct{ time.Time }
func (u UnixTime) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("%d", u.Unix())), nil
}Herramientas relacionadas
James is a systems engineer and Go enthusiast who focuses on high-performance microservices, command-line tooling, and infrastructure automation. He enjoys the simplicity and explicitness of Go and writes about building fast, reliable backend systems. When not coding he explores distributed systems concepts and contributes to open-source Go libraries.
Tobias is a platform engineer who builds developer tooling and internal infrastructure in Go. He has authored several open-source CLI tools and contributes to the Go toolchain ecosystem. He writes about the cobra and urfave/cli frameworks, cross-platform binary distribution, configuration management, and the patterns that make Go an ideal language for building reliable, self-contained command-line utilities.