Go encoding/json: JSON Marshaling and Unmarshaling
Go's `encoding/json` package provides robust functionality for converting Go data structures to JSON (marshaling) and JSON back to Go structures (unmarshaling). This bidirectional conversion is...
Key Insights
- Go’s
encoding/jsonpackage uses struct field tags to control JSON serialization behavior, with exported fields (capitalized) being the only ones marshaled by default - Custom marshaling logic requires implementing the
json.Marshalerandjson.Unmarshalerinterfaces, giving you complete control over how types are represented in JSON - For large JSON payloads, use
json.Decoderandjson.Encoderwith streaming instead of loading entire documents into memory withjson.Marshalandjson.Unmarshal
Introduction to JSON in Go
Go’s encoding/json package provides robust functionality for converting Go data structures to JSON (marshaling) and JSON back to Go structures (unmarshaling). This bidirectional conversion is essential for building APIs, working with configuration files, and integrating with external services.
The terminology is straightforward: marshaling transforms Go data into JSON bytes, while unmarshaling does the reverse. Unlike reflection-heavy approaches in some languages, Go’s JSON handling is type-safe and performant, though it requires understanding how Go’s type system maps to JSON’s more flexible structure.
Basic Marshaling: Structs to JSON
The json.Marshal() function converts Go values into JSON-encoded bytes. Only exported fields (those starting with capital letters) are included in the output—this is a fundamental Go principle that extends to JSON serialization.
package main
import (
"encoding/json"
"fmt"
"log"
)
type User struct {
Name string
Email string
Age int
password string // unexported, won't be marshaled
}
func main() {
user := User{
Name: "Alice Johnson",
Email: "alice@example.com",
Age: 32,
password: "secret123",
}
jsonData, err := json.Marshal(user)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(jsonData))
// Output: {"Name":"Alice Johnson","Email":"alice@example.com","Age":32}
}
Notice that password doesn’t appear in the output. For human-readable output, use json.MarshalIndent() which adds formatting with configurable prefix and indentation strings.
Basic Unmarshaling: JSON to Structs
Unmarshaling takes JSON bytes and populates a Go data structure. The destination must be a pointer so the function can modify the value. Go’s JSON unmarshaler is forgiving—it ignores unknown fields and leaves unmatched struct fields at their zero values.
func main() {
jsonStr := `{"Name":"Bob Smith","Email":"bob@example.com","Age":28,"Admin":true}`
var user User
err := json.Unmarshal([]byte(jsonStr), &user)
if err != nil {
log.Fatalf("Error unmarshaling JSON: %v", err)
}
fmt.Printf("Name: %s, Email: %s, Age: %d\n", user.Name, user.Email, user.Age)
// The "Admin" field in JSON is ignored since User struct doesn't have it
}
Type mismatches cause errors. If the JSON contains "Age":"not-a-number", unmarshaling will fail. Always check the error return value—silent failures lead to subtle bugs with zero values appearing where you expect data.
Struct Tags for JSON Control
Struct tags provide fine-grained control over JSON representation. The most common use is renaming fields to match JSON conventions (typically camelCase or snake_case).
type APIResponse struct {
UserID int `json:"user_id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
IsActive bool `json:"is_active,omitempty"`
Internal string `json:"-"`
Metadata string `json:",omitempty"`
}
func main() {
resp := APIResponse{
UserID: 42,
FirstName: "Jane",
LastName: "Doe",
IsActive: false,
Internal: "not serialized",
Metadata: "",
}
jsonData, _ := json.MarshalIndent(resp, "", " ")
fmt.Println(string(jsonData))
}
Output:
{
"user_id": 42,
"first_name": "Jane",
"last_name": "Doe"
}
The omitempty option excludes fields with zero values (0, false, nil, empty strings/slices/maps). The json:"-" tag completely ignores a field. When you use json:",omitempty" without a name, it keeps the Go field name but adds the omitempty behavior.
Working with Complex Data Types
Real-world applications deal with nested structures, slices, maps, and special types like time.Time. The JSON package handles these elegantly.
type Address struct {
Street string `json:"street"`
City string `json:"city"`
Country string `json:"country"`
}
type Order struct {
OrderID string `json:"order_id"`
Customer string `json:"customer"`
Items []string `json:"items"`
Total float64 `json:"total"`
Address Address `json:"address"`
Metadata map[string]string `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
func main() {
order := Order{
OrderID: "ORD-001",
Customer: "Alice",
Items: []string{"Widget", "Gadget", "Doohickey"},
Total: 299.99,
Address: Address{
Street: "123 Main St",
City: "Springfield",
Country: "USA",
},
Metadata: map[string]string{
"source": "web",
"promo": "SUMMER2024",
},
CreatedAt: time.Now(),
}
jsonData, _ := json.MarshalIndent(order, "", " ")
fmt.Println(string(jsonData))
}
The time.Time type marshals to RFC3339 format by default. Nil pointers marshal to JSON null, which is useful for optional fields. Empty slices marshal to [], while nil slices marshal to null—be aware of this distinction when designing APIs.
Custom Marshaling with Interfaces
When default JSON representation doesn’t fit your needs, implement the json.Marshaler and json.Unmarshaler interfaces. This is common for types that need special formatting or validation.
type Temperature float64
func (t Temperature) MarshalJSON() ([]byte, error) {
// Custom format: {"value": 72.5, "unit": "F"}
type tempJSON struct {
Value float64 `json:"value"`
Unit string `json:"unit"`
}
return json.Marshal(tempJSON{
Value: float64(t),
Unit: "F",
})
}
func (t *Temperature) UnmarshalJSON(data []byte) error {
type tempJSON struct {
Value float64 `json:"value"`
Unit string `json:"unit"`
}
var temp tempJSON
if err := json.Unmarshal(data, &temp); err != nil {
return err
}
// Convert to Fahrenheit if needed
if temp.Unit == "C" {
*t = Temperature(temp.Value*9/5 + 32)
} else {
*t = Temperature(temp.Value)
}
return nil
}
func main() {
temp := Temperature(72.5)
jsonData, _ := json.Marshal(temp)
fmt.Println(string(jsonData))
// Output: {"value":72.5,"unit":"F"}
var newTemp Temperature
json.Unmarshal([]byte(`{"value":100,"unit":"C"}`), &newTemp)
fmt.Printf("Temperature: %.1f°F\n", newTemp)
// Output: Temperature: 212.0°F
}
Be careful with custom unmarshalers—avoid infinite recursion by using a different type internally. The example uses an anonymous struct to prevent calling UnmarshalJSON recursively.
Best Practices and Common Pitfalls
For large JSON documents or streaming data, use json.Decoder instead of unmarshaling entire payloads into memory. This is critical for processing log files, API responses with pagination, or any scenario where memory efficiency matters.
func processLargeJSON(r io.Reader) error {
decoder := json.NewDecoder(r)
// Read opening bracket of array
_, err := decoder.Token()
if err != nil {
return err
}
// Process each object in the array
for decoder.More() {
var user User
if err := decoder.Decode(&user); err != nil {
return err
}
// Process user without loading entire array into memory
fmt.Printf("Processing user: %s\n", user.Name)
}
return nil
}
When dealing with unknown JSON structures, use map[string]interface{} or json.RawMessage to defer parsing. This is useful for APIs with dynamic schemas or when you only need specific fields.
Always validate unmarshaled data. JSON unmarshaling succeeds even if required fields are missing—they’ll just have zero values. Implement validation logic after unmarshaling:
type CreateUserRequest struct {
Username string `json:"username"`
Email string `json:"email"`
}
func (r *CreateUserRequest) Validate() error {
if r.Username == "" {
return errors.New("username is required")
}
if r.Email == "" {
return errors.New("email is required")
}
return nil
}
For security, never unmarshal untrusted JSON into interfaces without limits. A malicious payload could cause excessive memory allocation. Use json.Decoder with DisallowUnknownFields() for strict parsing, and consider setting size limits on input streams.
The encoding/json package is production-ready and handles the vast majority of use cases efficiently. For extreme performance requirements, consider third-party libraries like jsoniter or easyjson, but measure first—premature optimization often isn’t worth the added complexity.