
Howto: How to Use JSON in Go
JSON has become the de facto standard for data exchange in modern web applications, and Go’s built-in JSON package makes handling JSON data surprisingly straightforward. Whether you’re building REST APIs, consuming third-party services, or storing configuration data, mastering JSON manipulation in Go is essential for any serious backend development. This guide will walk you through everything from basic JSON marshaling and unmarshaling to advanced techniques like custom serialization, handling nested structures, and optimizing performance for high-throughput applications.
How JSON Works in Go
Go’s encoding/json
package uses reflection to automatically convert between Go structs and JSON. The process involves two main operations: marshaling (Go to JSON) and unmarshaling (JSON to Go). The package relies on struct tags to control how fields are serialized, and it follows specific rules for type conversions.
Here’s how Go maps JSON types to Go types:
JSON Type | Go Type | Notes |
---|---|---|
string | string | Direct mapping |
number | float64, int, int64 | Default is float64 for interface{} |
boolean | bool | Direct mapping |
null | nil | Works with pointers and interfaces |
array | []interface{}, []T | Slice of appropriate type |
object | map[string]interface{}, struct | Struct preferred for known structure |
Basic JSON Operations
Let’s start with the fundamentals. Here’s a simple example showing basic marshaling and unmarshaling:
package main
import (
"encoding/json"
"fmt"
"log"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
IsActive bool `json:"is_active"`
}
func main() {
// Create a user
user := User{
ID: 1,
Name: "John Doe",
Email: "john@example.com",
IsActive: true,
}
// Marshal to JSON
jsonData, err := json.Marshal(user)
if err != nil {
log.Fatal(err)
}
fmt.Printf("JSON: %s\n", jsonData)
// Unmarshal back to struct
var newUser User
err = json.Unmarshal(jsonData, &newUser)
if err != nil {
log.Fatal(err)
}
fmt.Printf("User: %+v\n", newUser)
}
The struct tags are crucial here. They control the JSON field names and behavior. Common tag options include:
json:"field_name"
– Specify JSON field namejson:"-"
– Ignore field completelyjson:",omitempty"
– Omit field if emptyjson:",string"
– Convert to/from JSON string
Working with Dynamic JSON
Sometimes you don’t know the JSON structure ahead of time. Go provides several approaches for handling dynamic JSON:
package main
import (
"encoding/json"
"fmt"
"log"
)
func handleDynamicJSON() {
jsonString := `{
"name": "Alice",
"age": 30,
"hobbies": ["reading", "swimming"],
"address": {
"city": "New York",
"zipcode": "10001"
}
}`
// Method 1: Using map[string]interface{}
var data map[string]interface{}
err := json.Unmarshal([]byte(jsonString), &data)
if err != nil {
log.Fatal(err)
}
// Accessing nested data requires type assertions
name := data["name"].(string)
age := data["age"].(float64) // JSON numbers are float64
hobbies := data["hobbies"].([]interface{})
address := data["address"].(map[string]interface{})
city := address["city"].(string)
fmt.Printf("Name: %s, Age: %.0f, City: %s\n", name, age, city)
fmt.Printf("Hobbies: %v\n", hobbies)
}
func handleWithRawMessage() {
jsonString := `{
"type": "user",
"data": {"id": 123, "name": "Bob"}
}`
var envelope struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
err := json.Unmarshal([]byte(jsonString), &envelope)
if err != nil {
log.Fatal(err)
}
// Process based on type
switch envelope.Type {
case "user":
var user struct {
ID int `json:"id"`
Name string `json:"name"`
}
err = json.Unmarshal(envelope.Data, &user)
if err != nil {
log.Fatal(err)
}
fmt.Printf("User: %+v\n", user)
}
}
Advanced JSON Techniques
For more complex scenarios, you might need custom marshaling logic. Go allows you to implement the json.Marshaler
and json.Unmarshaler
interfaces:
package main
import (
"encoding/json"
"fmt"
"strconv"
"time"
)
// Custom time format
type CustomTime struct {
time.Time
}
func (ct *CustomTime) UnmarshalJSON(data []byte) error {
// Remove quotes from JSON string
s := string(data[1 : len(data)-1])
// Parse custom format
t, err := time.Parse("2006-01-02", s)
if err != nil {
return err
}
ct.Time = t
return nil
}
func (ct CustomTime) MarshalJSON() ([]byte, error) {
return json.Marshal(ct.Time.Format("2006-01-02"))
}
// Custom number handling
type FlexibleNumber struct {
Value int
}
func (fn *FlexibleNumber) UnmarshalJSON(data []byte) error {
// Handle both string and number representations
var v interface{}
if err := json.Unmarshal(data, &v); err != nil {
return err
}
switch val := v.(type) {
case float64:
fn.Value = int(val)
case string:
i, err := strconv.Atoi(val)
if err != nil {
return err
}
fn.Value = i
default:
return fmt.Errorf("cannot unmarshal %T into FlexibleNumber", val)
}
return nil
}
type Event struct {
Name string `json:"name"`
Date CustomTime `json:"date"`
Priority FlexibleNumber `json:"priority"`
}
func demonstrateCustomMarshaling() {
jsonData := `{
"name": "Conference",
"date": "2024-06-15",
"priority": "1"
}`
var event Event
err := json.Unmarshal([]byte(jsonData), &event)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Event: %s on %s (Priority: %d)\n",
event.Name,
event.Date.Format("January 2, 2006"),
event.Priority.Value)
}
Performance Optimization
JSON processing can become a bottleneck in high-performance applications. Here are some optimization strategies:
package main
import (
"encoding/json"
"fmt"
"strings"
"time"
)
// Use json.Decoder for streaming large JSON
func processLargeJSONStream(jsonData string) {
decoder := json.NewDecoder(strings.NewReader(jsonData))
// Read opening bracket
token, err := decoder.Token()
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Token: %v\n", token)
// Process array elements one by one
for decoder.More() {
var item map[string]interface{}
err := decoder.Decode(&item)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
// Process item without loading entire array into memory
fmt.Printf("Processing item: %v\n", item)
}
}
// Reuse structures to reduce allocations
type JSONProcessor struct {
buffer []byte
user User
}
func (jp *JSONProcessor) ProcessUser(jsonData []byte) (*User, error) {
// Reset the user struct
jp.user = User{}
err := json.Unmarshal(jsonData, &jp.user)
if err != nil {
return nil, err
}
return &jp.user, nil
}
// Benchmark comparison
func benchmarkJSONOperations() {
user := User{ID: 1, Name: "Test User", Email: "test@example.com", IsActive: true}
// Standard approach
start := time.Now()
for i := 0; i < 10000; i++ {
data, _ := json.Marshal(user)
var newUser User
json.Unmarshal(data, &newUser)
}
standardTime := time.Since(start)
// Optimized with reuse
processor := &JSONProcessor{}
start = time.Now()
for i := 0; i < 10000; i++ {
data, _ := json.Marshal(user)
processor.ProcessUser(data)
}
optimizedTime := time.Since(start)
fmt.Printf("Standard: %v, Optimized: %v\n", standardTime, optimizedTime)
}
Real-World Use Cases
Here are practical examples you'll encounter in production systems:
// API Response handling
type APIResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
Meta struct {
Page int `json:"page"`
PerPage int `json:"per_page"`
Total int `json:"total"`
} `json:"meta,omitempty"`
}
// Configuration management
type ServerConfig struct {
Host string `json:"host"`
Port int `json:"port"`
Database DatabaseConfig `json:"database"`
Cache CacheConfig `json:"cache"`
Features map[string]bool `json:"features"`
Timeouts TimeoutConfig `json:"timeouts"`
}
type DatabaseConfig struct {
Host string `json:"host"`
Port int `json:"port"`
Name string `json:"name"`
Username string `json:"username"`
Password string `json:"password,omitempty"` // Omit in logs
}
// Webhook payload processing
type WebhookPayload struct {
Event string `json:"event"`
Timestamp time.Time `json:"timestamp"`
Data map[string]interface{} `json:"data"`
Signature string `json:"signature"`
}
func processWebhook(payload []byte) error {
var webhook WebhookPayload
if err := json.Unmarshal(payload, &webhook); err != nil {
return fmt.Errorf("invalid webhook payload: %w", err)
}
// Validate signature, process based on event type
switch webhook.Event {
case "user.created":
return handleUserCreated(webhook.Data)
case "payment.completed":
return handlePaymentCompleted(webhook.Data)
default:
return fmt.Errorf("unknown event type: %s", webhook.Event)
}
}
Common Pitfalls and Troubleshooting
Even experienced developers run into JSON-related issues. Here are the most common problems and solutions:
- Unexported fields: Go only marshals exported (capitalized) struct fields. Use struct tags to control JSON field names.
- Type assertions on interface{}: Always check type assertions or use the comma ok idiom to avoid panics.
- Number precision: JSON numbers are unmarshaled as float64 by default, which can cause precision issues with large integers.
- Empty vs nil slices: JSON marshaling treats nil slices as null and empty slices as [].
// Safe type assertion
func safeTypeAssertion(data map[string]interface{}) {
if name, ok := data["name"].(string); ok {
fmt.Printf("Name: %s\n", name)
} else {
fmt.Println("Name field is not a string or doesn't exist")
}
}
// Handle large integers properly
type LargeNumberExample struct {
ID json.Number `json:"id"` // Preserves precision
}
func handleLargeNumbers() {
jsonData := `{"id": 9223372036854775807}`
var example LargeNumberExample
err := json.Unmarshal([]byte(jsonData), &example)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
// Convert to int64 safely
id, err := example.ID.Int64()
if err != nil {
fmt.Printf("Error converting to int64: %v\n", err)
return
}
fmt.Printf("Large ID: %d\n", id)
}
Best Practices
Follow these guidelines for robust JSON handling in production applications:
- Always validate JSON input, especially from external sources
- Use struct tags consistently across your codebase
- Implement custom marshaling for complex types like time.Time
- Consider using json.Number for financial or high-precision numeric data
- Use json.Decoder for streaming large JSON files
- Handle errors gracefully and provide meaningful error messages
- Use omitempty for optional fields to reduce payload size
- Consider using third-party libraries like jsoniter for performance-critical applications
For comprehensive documentation and advanced features, refer to the official Go JSON package documentation. The JSON and Go article provides additional insights into the design decisions behind Go's JSON package.
Mastering JSON in Go opens up countless possibilities for building robust web services, APIs, and data processing applications. Start with the basics, understand the type system, and gradually incorporate advanced techniques as your applications grow in complexity.

This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.
This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.