Work with JSON Effectively
Problem
JSON handling requires proper struct tags, custom marshaling for complex types, and validation. Incorrect marshaling leads to data loss or incorrect JSON output.
// Problematic - no control over JSON output
type User struct {
ID int
Password string // Exposed in JSON!
Email string
}This guide shows practical JSON handling techniques in Go.
Solution
1. Struct Tags and Basic Marshaling
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"-"` // Omit from JSON
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at,omitempty"` // Omit if zero
}
func main() {
user := User{
ID: 1,
Username: "john",
Email: "john@example.com",
Password: "secret",
}
// Marshal to JSON
data, _ := json.Marshal(user)
fmt.Println(string(data))
// {"id":1,"username":"john","email":"john@example.com","created_at":"0001-01-01T00:00:00Z"}
// Unmarshal from JSON
jsonStr := `{"id":2,"username":"jane","email":"jane@example.com"}`
var newUser User
json.Unmarshal([]byte(jsonStr), &newUser)
}2. Custom JSON Marshaling
type Duration time.Duration
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Duration(d).String())
}
func (d *Duration) UnmarshalJSON(b []byte) error {
var v interface{}
if err := json.Unmarshal(b, &v); err != nil {
return err
}
switch value := v.(type) {
case string:
dur, err := time.ParseDuration(value)
if err != nil {
return err
}
*d = Duration(dur)
return nil
default:
return errors.New("invalid duration")
}
}
type Config struct {
Timeout Duration `json:"timeout"`
}3. Embedded JSON
type Event struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
type UserEvent struct {
Username string `json:"username"`
Email string `json:"email"`
}
func handleEvent(eventJSON string) error {
var event Event
if err := json.Unmarshal([]byte(eventJSON), &event); err != nil {
return err
}
switch event.Type {
case "user_created":
var userEvent UserEvent
if err := json.Unmarshal(event.Data, &userEvent); err != nil {
return err
}
fmt.Printf("User created: %s\n", userEvent.Username)
}
return nil
}How It Works
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC
graph TD
A[Go Struct] --> B{Marshal}
B --> C[JSON Encoder]
C --> D[JSON Bytes]
E[JSON Bytes] --> F{Unmarshal}
F --> G[JSON Decoder]
G --> H[Go Struct]
style A fill:#0173B2,stroke:#000000,color:#FFFFFF
style D fill:#DE8F05,stroke:#000000,color:#FFFFFF
style G fill:#029E73,stroke:#000000,color:#FFFFFF
style H fill:#CC78BC,stroke:#000000,color:#FFFFFF
Variations
1. JSON Streaming with Encoder/Decoder
Use json.Encoder and json.Decoder for streaming large JSON:
// Streaming encode
func streamEncode(w io.Writer, users []User) error {
enc := json.NewEncoder(w)
enc.SetIndent("", " ") // Pretty print
for _, user := range users {
if err := enc.Encode(user); err != nil {
return err
}
}
return nil
}
// Streaming decode
func streamDecode(r io.Reader) ([]User, error) {
dec := json.NewDecoder(r)
var users []User
// Read opening bracket
if _, err := dec.Token(); err != nil {
return nil, err
}
for dec.More() {
var user User
if err := dec.Decode(&user); err != nil {
return nil, err
}
users = append(users, user)
}
// Read closing bracket
if _, err := dec.Token(); err != nil {
return nil, err
}
return users, nil
}Trade-offs: Memory efficient for large data but more complex than Marshal/Unmarshal.
2. Nullable Fields with Pointers
Use pointers to distinguish between zero values and missing fields:
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Age *int `json:"age,omitempty"` // nil if not provided
Active *bool `json:"active,omitempty"` // nil vs false
}
func main() {
age := 25
active := false
user := User{
ID: 1,
Username: "john",
Age: &age, // Explicitly set to 25
Active: &active, // Explicitly set to false (not omitted)
}
data, _ := json.Marshal(user)
// {"id":1,"username":"john","age":25,"active":false}
}Trade-offs: Precise control over null vs zero but adds pointer dereferencing.
3. Custom Field Names with Tags
Map Go names to different JSON names:
type APIResponse struct {
UserID int `json:"user_id"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
IsActive bool `json:"is_active"`
}Trade-offs: Adapts to external APIs but creates naming inconsistency.
4. Validation with JSON Schema
Validate JSON against schema:
import "github.com/xeipuuv/gojsonschema"
func validateJSON(jsonData, schemaURL string) error {
schema := gojsonschema.NewReferenceLoader(schemaURL)
doc := gojsonschema.NewStringLoader(jsonData)
result, err := gojsonschema.Validate(schema, doc)
if err != nil {
return err
}
if !result.Valid() {
for _, desc := range result.Errors() {
return fmt.Errorf("validation error: %s", desc)
}
}
return nil
}Trade-offs: Strong validation but requires schema definition and external library.
5. Dynamic JSON with map[string]interface
Handle unknown JSON structure:
func parseDynamicJSON(jsonStr string) error {
var data map[string]interface{}
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
return err
}
// Type assertions needed
if username, ok := data["username"].(string); ok {
fmt.Println("Username:", username)
}
if age, ok := data["age"].(float64); ok { // JSON numbers are float64!
fmt.Println("Age:", int(age))
}
return nil
}Trade-offs: Flexible but loses type safety and requires type assertions.
Common Pitfalls
Pitfall 1: Unexported Fields
// Bad - lowercase fields won't marshal
type User struct {
id int
name string
}
// Good
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}Pitfall 2: Ignoring Errors
// Bad
json.Unmarshal(data, &user)
// Good
if err := json.Unmarshal(data, &user); err != nil {
return fmt.Errorf("unmarshal failed: %w", err)
}Related Patterns
Related Tutorial: See Beginner Tutorial - JSON Basics. Related How-To: See Build REST APIs. Related Cookbook: See Cookbook recipe “JSON Patterns”.