JSON Handling in Go
JSON (JavaScript Object Notation) is the most common data format for web APIs. Go’s encoding/json package provides comprehensive support for encoding and decoding JSON data.
Basic Structs and JSON
Define Go structs that map to JSON:
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email,omitempty"` // Omit if empty
}
func main() {
// Create a person
person := Person{
Name: "Alice",
Age: 30,
Email: "[email protected]",
}
// Encode to JSON
jsonData, err := json.Marshal(person)
if err != nil {
fmt.Println("Error encoding JSON:", err)
return
}
fmt.Println("JSON:", string(jsonData))
// Decode from JSON
jsonString := `{"name":"Bob","age":25,"email":"[email protected]"}`
var decodedPerson Person
err = json.Unmarshal([]byte(jsonString), &decodedPerson)
if err != nil {
fmt.Println("Error decoding JSON:", err)
return
}
fmt.Printf("Decoded: %+v\n", decodedPerson)
}JSON Tags
Control JSON serialization with struct tags:
package main
import (
"encoding/json"
"fmt"
)
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Password string `json:"-"` // Never serialize
Email string `json:"email,omitempty"`
Created string `json:"created_at"`
}
func main() {
user := User{
ID: 1,
Username: "johndoe",
Password: "secret123",
Email: "[email protected]",
Created: "2023-01-01",
}
jsonData, _ := json.MarshalIndent(user, "", " ")
fmt.Println(string(jsonData))
}Output:
{
"id": 1,
"username": "johndoe",
"email": "[email protected]",
"created_at": "2023-01-01"
}Nested Structs
Handle complex nested structures:
package main
import (
"encoding/json"
"fmt"
)
type Address struct {
Street string `json:"street"`
City string `json:"city"`
Country string `json:"country"`
}
type Company struct {
Name string `json:"name"`
Address Address `json:"address"`
}
type Employee struct {
Name string `json:"name"`
Age int `json:"age"`
Company Company `json:"company"`
}
func main() {
employee := Employee{
Name: "Alice",
Age: 30,
Company: Company{
Name: "Tech Corp",
Address: Address{
Street: "123 Main St",
City: "New York",
Country: "USA",
},
},
}
jsonData, _ := json.MarshalIndent(employee, "", " ")
fmt.Println(string(jsonData))
}Arrays and Slices
Working with JSON arrays:
package main
import (
"encoding/json"
"fmt"
)
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
}
func main() {
// Array of products
products := []Product{
{ID: 1, Name: "Laptop", Price: 999.99},
{ID: 2, Name: "Mouse", Price: 29.99},
{ID: 3, Name: "Keyboard", Price: 79.99},
}
// Encode to JSON
jsonData, err := json.Marshal(products)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Products JSON:", string(jsonData))
// Decode from JSON
jsonString := `[
{"id": 4, "name": "Monitor", "price": 299.99},
{"id": 5, "name": "Headphones", "price": 149.99}
]`
var decodedProducts []Product
err = json.Unmarshal([]byte(jsonString), &decodedProducts)
if err != nil {
fmt.Println("Error:", err)
return
}
for _, p := range decodedProducts {
fmt.Printf("Product: %+v\n", p)
}
}Maps
Using maps for flexible JSON structures:
package main
import (
"encoding/json"
"fmt"
)
func main() {
// Map with string keys
person := map[string]interface{}{
"name": "Charlie",
"age": 35,
"email": "[email protected]",
"skills": []string{"Go", "Python", "JavaScript"},
}
jsonData, _ := json.MarshalIndent(person, "", " ")
fmt.Println("Map JSON:")
fmt.Println(string(jsonData))
// Decode into map
jsonString := `{"name":"Diana","department":"Engineering","salary":75000}`
var employee map[string]interface{}
err := json.Unmarshal([]byte(jsonString), &employee)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Name:", employee["name"])
fmt.Println("Department:", employee["department"])
fmt.Println("Salary:", employee["salary"])
}Custom JSON Parsing
Handle dynamic or complex JSON structures:
package main
import (
"encoding/json"
"fmt"
)
func parseUnknownJSON(jsonData string) {
var data interface{}
err := json.Unmarshal([]byte(jsonData), &data)
if err != nil {
fmt.Println("Error:", err)
return
}
parseValue(data, "")
}
func parseValue(value interface{}, prefix string) {
switch v := value.(type) {
case map[string]interface{}:
for key, val := range v {
parseValue(val, prefix+key+".")
}
case []interface{}:
for i, val := range v {
parseValue(val, fmt.Sprintf("%s[%d].", prefix, i))
}
default:
fmt.Printf("%s = %v (%T)\n", prefix[:len(prefix)-1], v, v)
}
}
func main() {
jsonString := `{
"user": {
"name": "Eve",
"preferences": {
"theme": "dark",
"notifications": true
},
"tags": ["golang", "developer"]
}
}`
parseUnknownJSON(jsonString)
}Custom Marshaling
Implement custom JSON marshaling:
package main
import (
"encoding/json"
"fmt"
"strings"
"time"
)
type Event struct {
ID int `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Custom marshaling for Event
func (e Event) MarshalJSON() ([]byte, error) {
type Alias Event // Prevent infinite recursion
return json.Marshal(&struct {
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
*Alias
}{
CreatedAt: e.CreatedAt.Format(time.RFC3339),
UpdatedAt: e.UpdatedAt.Format(time.RFC3339),
Alias: (*Alias)(&e),
})
}
// Custom unmarshaling
func (e *Event) UnmarshalJSON(data []byte) error {
type Alias Event
aux := &struct {
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
*Alias
}{
Alias: (*Alias)(e),
}
if err := json.Unmarshal(data, aux); err != nil {
return err
}
var err error
e.CreatedAt, err = time.Parse(time.RFC3339, aux.CreatedAt)
if err != nil {
return err
}
e.UpdatedAt, err = time.Parse(time.RFC3339, aux.UpdatedAt)
return err
}
func main() {
event := Event{
ID: 1,
Title: "Go Workshop",
Description: "Learn Go programming",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
jsonData, _ := json.MarshalIndent(event, "", " ")
fmt.Println("Custom JSON:")
fmt.Println(string(jsonData))
}Streaming JSON
For large JSON data, use streaming:
package main
import (
"encoding/json"
"fmt"
"io"
"strings"
)
func main() {
jsonData := `[
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25},
{"name": "Charlie", "age": 35}
]`
decoder := json.NewDecoder(strings.NewReader(jsonData))
// Read opening bracket
_, err := decoder.Token()
if err != nil {
fmt.Println("Error:", err)
return
}
// Read array elements
for decoder.More() {
var person map[string]interface{}
err := decoder.Decode(&person)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Person: %+v\n", person)
}
// Read closing bracket
_, err = decoder.Token()
if err != nil {
fmt.Println("Error:", err)
}
}JSON Lines (NDJSON)
Handle JSON Lines format:
package main
import (
"bufio"
"encoding/json"
"fmt"
"strings"
)
type LogEntry struct {
Timestamp string `json:"timestamp"`
Level string `json:"level"`
Message string `json:"message"`
}
func main() {
jsonLines := `{"timestamp":"2023-01-01T10:00:00Z","level":"INFO","message":"Application started"}
{"timestamp":"2023-01-01T10:01:00Z","level":"WARN","message":"High memory usage"}
{"timestamp":"2023-01-01T10:02:00Z","level":"ERROR","message":"Database connection failed"}`
scanner := bufio.NewScanner(strings.NewReader(jsonLines))
for scanner.Scan() {
var entry LogEntry
err := json.Unmarshal([]byte(scanner.Text()), &entry)
if err != nil {
fmt.Println("Error parsing line:", err)
continue
}
fmt.Printf("[%s] %s: %s\n", entry.Level, entry.Timestamp, entry.Message)
}
}Error Handling
Robust error handling for JSON operations:
package main
import (
"encoding/json"
"fmt"
)
func safeJSONOperation(jsonStr string) {
var data interface{}
// Check for valid JSON first
if !json.Valid([]byte(jsonStr)) {
fmt.Println("Invalid JSON format")
return
}
err := json.Unmarshal([]byte(jsonStr), &data)
if err != nil {
fmt.Println("Error parsing JSON:", err)
return
}
// Type assertion with check
if obj, ok := data.(map[string]interface{}); ok {
if name, exists := obj["name"]; exists {
if nameStr, ok := name.(string); ok {
fmt.Println("Name:", nameStr)
} else {
fmt.Println("Name is not a string")
}
} else {
fmt.Println("Name field not found")
}
} else {
fmt.Println("JSON is not an object")
}
}
func main() {
safeJSONOperation(`{"name": "Test", "value": 123}`)
safeJSONOperation(`invalid json`)
safeJSONOperation(`{"name": 456}`) // Name is not a string
}Best Practices
- Use struct tags: Control JSON field names and behavior
- Handle errors: Always check for JSON parsing errors
- Validate input: Use
json.Valid()for untrusted input - Use pointers for optional fields: Use
*stringfor nullable fields - Avoid interface{}: Use concrete types when possible
- Use MarshalIndent for debugging: Pretty-print JSON for development
- Consider streaming: For large JSON data
- Handle time fields properly: Use custom marshaling for timestamps
- Use consistent naming: Follow JSON naming conventions
- Test JSON operations: Include JSON tests in your test suite
Go’s JSON package is powerful and efficient. With proper struct definitions and error handling, you can work with JSON data reliably in your Go applications.
For more on file operations, check our file I/O tutorial. If you need to build HTTP clients, see the HTTP clients tutorial.