JSON Formatter Go — Guía de MarshalIndent()

·Systems Engineer·Revisado porTobias Müller·Publicado

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.

Before · json
After · json
{"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.

Go — ejemplo mínimo funcional
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:

Go — tabulaciones vs espacios
// 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
// }
Nota:Usa 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.

Go — struct tags para una respuesta de API
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.

Go — excluir campos sensibles
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 aparecen
Nota:Los campos de struct no exportados (en minúsculas) siempre son excluidos por encoding/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

Go — MarshalJSON personalizado para Money
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

Go — MarshalJSON personalizado para enum de estado
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"
// }
Nota:Implementa siempre 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.

Go 1.21+ — serialización de UUID
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"
// }
Nota:Si almacenas UUIDs como [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.

Parámetro
Tipo
Descripción
v
any
El valor a serializar — struct, map, slice o primitivo
prefix
string
Cadena antepuesta a cada línea de salida (normalmente "")
indent
string
Cadena usada por cada nivel de indentación ("\t" o " ")

Opciones comunes de struct tags:

Tag
Efecto
json:"name"
Renombra el campo a name en la salida JSON
json:"name,omitempty"
Renombra + omite si el valor es cero (nil, "", 0, false)
json:"-"
Excluye siempre este campo de la salida JSON
json:",string"
Codifica un número o bool como cadena JSON

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.

Go — json.Indent sobre bytes en bruto
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.

Go — json.Indent para logging de depuración
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())
}
Aviso: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

Go — formatear archivo JSON en el lugar
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

Go — formatear respuesta HTTP para log 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)

Go — formatear cuerpo HTTP en bruto con io.ReadAll
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.

Go — dos patrones en paralelo
// 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.

bash — pasar JSON al formateador integrado de Python
echo '{"service":"payments","port":8443,"workers":4}' | python3 -m json.tool
# {
#     "service": "payments",
#     "port": 8443,
#     "workers": 4
# }
bash — pasar a jq para formateo y filtrado completos
# 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")'
bash — pasar a un main.go mínimo por stdin
# 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.go
Nota:gofmt 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.

bash — instalar go-json
go get github.com/goccy/go-json
Go — sustituir encoding/json por go-json con un solo cambio de import
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

Go — stream de array JSON grande sin cargar en memoria
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

Go — procesar stream NDJSON línea 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)
	}
}
Nota:Cambia a streaming cuando el archivo supere los 100 MB o cuando proceses streams no acotados (consumidores Kafka, pipelines de log, lecturas de objetos S3). Cargar un archivo JSON de 500 MB con 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

Ignorar el valor de error devuelto por MarshalIndent

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.

Before · Go
After · Go
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))
Usar fmt.Println en lugar de os.Stdout.Write para salida binaria

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.

Before · Go
After · Go
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 necesario
Olvidar omitempty en campos puntero

Problema: 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.

Before · Go
After · Go
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"}
Usar map[string]interface{} en lugar de structs

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.

Before · Go
After · Go
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ápido

encoding/json vs alternativas — Comparación rápida

Método
Salida formateada
JSON válido
Tipos personalizados
Streaming
Requiere instalación
json.MarshalIndent
✓ vía MarshalJSON
No (stdlib)
json.Indent
N/A (solo bytes)
No (stdlib)
json.Encoder
✗ (compacto)
✓ vía MarshalJSON
No (stdlib)
go-json
go get
sonic
go get (amd64/arm64)
jq (CLI)
N/A
Instalación del sistema

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.

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.

Go
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.

Go
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.

Go
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.

Go
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.

Go
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

También disponible en:PythonJavaScriptBash
JO
James OkaforSystems Engineer

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.

TM
Tobias MüllerRevisor técnico

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.