Exercise: JSON Handler
Difficulty - Beginner
Learning Objectives
- Master JSON encoding and decoding in Go
- Practice working with struct tags
- Learn to handle nested JSON structures
- Understand JSON marshaling patterns
Problem Statement
Create a program that works with a simple user management system using JSON. You'll handle JSON encoding/decoding, validation, and file persistence.
Implement these core functions:
- User struct with proper JSON tags
- SaveToJSON: Save users to a JSON file
- LoadFromJSON: Load users from a JSON file
- FindByID: Find a user by ID
- FilterByAge: Filter users by minimum age
Data Structures
1package userstore
2
3import "time"
4
5type User struct {
6 ID int `json:"id"`
7 Username string `json:"username"`
8 Email string `json:"email"`
9 Age int `json:"age"`
10 Active bool `json:"active"`
11 CreatedAt time.Time `json:"created_at"`
12}
13
14type UserStore struct {
15 Users []User `json:"users"`
16}
Function Signatures
1// SaveToJSON writes users to a JSON file
2func SaveToJSON(filename string) error
3
4// LoadFromJSON reads users from a JSON file
5func LoadFromJSON(filename string) error
6
7// AddUser adds a new user to the store
8func AddUser(user User) error
9
10// FindByID finds a user by their ID
11func FindByID(id int)
12
13// FilterByAge returns users with age >= minAge
14func FilterByAge(minAge int) []User
15
16// ToJSON converts the store to JSON string
17func ToJSON()
18
19// FromJSON populates the store from a JSON string
20func FromJSON(jsonStr string) error
Example Usage
1package main
2
3import (
4 "fmt"
5 "log"
6 "time"
7 "userstore"
8)
9
10func main() {
11 store := &userstore.UserStore{}
12
13 // Add users
14 users := []userstore.User{
15 {
16 ID: 1,
17 Username: "alice",
18 Email: "alice@example.com",
19 Age: 28,
20 Active: true,
21 CreatedAt: time.Now(),
22 },
23 {
24 ID: 2,
25 Username: "bob",
26 Email: "bob@example.com",
27 Age: 35,
28 Active: true,
29 CreatedAt: time.Now(),
30 },
31 {
32 ID: 3,
33 Username: "charlie",
34 Email: "charlie@example.com",
35 Age: 17,
36 Active: false,
37 CreatedAt: time.Now(),
38 },
39 }
40
41 for _, user := range users {
42 if err := store.AddUser(user); err != nil {
43 log.Fatal(err)
44 }
45 }
46
47 // Save to file
48 if err := store.SaveToJSON("users.json"); err != nil {
49 log.Fatal(err)
50 }
51 fmt.Println("Users saved to users.json")
52
53 // Load from file
54 newStore := &userstore.UserStore{}
55 if err := newStore.LoadFromJSON("users.json"); err != nil {
56 log.Fatal(err)
57 }
58 fmt.Printf("Loaded %d users\n", len(newStore.Users))
59
60 // Find by ID
61 user, err := newStore.FindByID(2)
62 if err != nil {
63 log.Fatal(err)
64 }
65 fmt.Printf("Found user: %s\n", user.Username)
66
67 // Filter by age
68 adults := newStore.FilterByAge(18)
69 fmt.Printf("Found %d adult users\n", len(adults))
70
71 // Convert to JSON string
72 jsonStr, err := store.ToJSON()
73 if err != nil {
74 log.Fatal(err)
75 }
76 fmt.Println("JSON:", jsonStr)
77}
Requirements
- Use proper JSON struct tags
- Handle duplicate IDs when adding users
- Return appropriate errors for not found cases
- Pretty-print JSON when saving to file
- Handle empty/non-existent files gracefully
Solution
Click to see the complete solution
1package userstore
2
3import (
4 "encoding/json"
5 "fmt"
6 "os"
7 "time"
8)
9
10type User struct {
11 ID int `json:"id"`
12 Username string `json:"username"`
13 Email string `json:"email"`
14 Age int `json:"age"`
15 Active bool `json:"active"`
16 CreatedAt time.Time `json:"created_at"`
17}
18
19type UserStore struct {
20 Users []User `json:"users"`
21}
22
23// SaveToJSON writes users to a JSON file
24func SaveToJSON(filename string) error {
25 data, err := json.MarshalIndent(us, "", " ")
26 if err != nil {
27 return fmt.Errorf("marshaling JSON: %w", err)
28 }
29
30 if err := os.WriteFile(filename, data, 0644); err != nil {
31 return fmt.Errorf("writing file: %w", err)
32 }
33
34 return nil
35}
36
37// LoadFromJSON reads users from a JSON file
38func LoadFromJSON(filename string) error {
39 data, err := os.ReadFile(filename)
40 if err != nil {
41 return fmt.Errorf("reading file: %w", err)
42 }
43
44 if err := json.Unmarshal(data, us); err != nil {
45 return fmt.Errorf("unmarshaling JSON: %w", err)
46 }
47
48 return nil
49}
50
51// AddUser adds a new user to the store
52func AddUser(user User) error {
53 // Check for duplicate ID
54 for _, existingUser := range us.Users {
55 if existingUser.ID == user.ID {
56 return fmt.Errorf("user with ID %d already exists", user.ID)
57 }
58 }
59
60 us.Users = append(us.Users, user)
61 return nil
62}
63
64// FindByID finds a user by their ID
65func FindByID(id int) {
66 for i := range us.Users {
67 if us.Users[i].ID == id {
68 return &us.Users[i], nil
69 }
70 }
71
72 return nil, fmt.Errorf("user with ID %d not found", id)
73}
74
75// FilterByAge returns users with age >= minAge
76func FilterByAge(minAge int) []User {
77 var filtered []User
78
79 for _, user := range us.Users {
80 if user.Age >= minAge {
81 filtered = append(filtered, user)
82 }
83 }
84
85 return filtered
86}
87
88// ToJSON converts the store to a JSON string
89func ToJSON() {
90 data, err := json.MarshalIndent(us, "", " ")
91 if err != nil {
92 return "", fmt.Errorf("marshaling JSON: %w", err)
93 }
94
95 return string(data), nil
96}
97
98// FromJSON populates the store from a JSON string
99func FromJSON(jsonStr string) error {
100 if err := json.Unmarshal([]byte(jsonStr), us); err != nil {
101 return fmt.Errorf("unmarshaling JSON: %w", err)
102 }
103
104 return nil
105}
Explanation
JSON Struct Tags:
json:"field_name"specifies the JSON field name- Lowercase JSON fields follow JSON naming conventions
- Tags are essential for proper marshaling/unmarshaling
SaveToJSON:
- Uses
json.MarshalIndent()for pretty-printed JSON - Two-space indentation for readability
- Wraps errors with context
LoadFromJSON:
- Reads entire file with
os.ReadFile() - Uses
json.Unmarshal()to populate struct - Returns clear error messages
AddUser:
- Validates for duplicate IDs before adding
- Returns error if duplicate exists
- Simple O(n) duplicate check
FindByID:
- Returns pointer to user to avoid copying
- Returns error if user not found
- Iterates through Users slice
FilterByAge:
- Creates new slice with filtered users
- Does not modify original store
- Returns empty slice if no matches
JSON Struct Tag Options
1type User struct {
2 // Omit field if empty
3 Email string `json:"email,omitempty"`
4
5 // Rename field
6 Username string `json:"user_name"`
7
8 // Ignore field completely
9 Password string `json:"-"`
10
11 // Use string encoding for numbers
12 Age int `json:"age,string"`
13}
Common JSON Patterns
Handling Unknown Fields:
1type Config struct {
2 Known string `json:"known"`
3 Extra map[string]interface{} `json:"-"`
4}
5
6func UnmarshalJSON(data []byte) error {
7 type Alias Config
8 aux := &struct {
9 *Alias
10 }{
11 Alias:(c),
12 }
13
14 if err := json.Unmarshal(data, aux); err != nil {
15 return err
16 }
17
18 // Handle unknown fields in c.Extra
19 return nil
20}
Custom JSON Marshaling:
1func MarshalJSON() {
2 type Alias User
3 return json.Marshal(&struct {
4 *Alias
5 CreatedAtFormatted string `json:"created_at_formatted"`
6 }{
7 Alias: (u),
8 CreatedAtFormatted: u.CreatedAt.Format(time.RFC3339),
9 })
10}
Test Cases
1package userstore
2
3import (
4 "os"
5 "testing"
6 "time"
7)
8
9func TestAddUser(t *testing.T) {
10 store := &UserStore{}
11
12 user := User{
13 ID: 1,
14 Username: "test",
15 Email: "test@example.com",
16 Age: 25,
17 Active: true,
18 }
19
20 // Should succeed
21 if err := store.AddUser(user); err != nil {
22 t.Fatalf("AddUser failed: %v", err)
23 }
24
25 // Should fail
26 if err := store.AddUser(user); err == nil {
27 t.Error("AddUser should fail for duplicate ID")
28 }
29}
30
31func TestSaveAndLoad(t *testing.T) {
32 filename := "test_users.json"
33 defer os.Remove(filename)
34
35 store := &UserStore{}
36 store.AddUser(User{
37 ID: 1,
38 Username: "alice",
39 Email: "alice@example.com",
40 Age: 28,
41 Active: true,
42 CreatedAt: time.Now(),
43 })
44
45 // Save
46 if err := store.SaveToJSON(filename); err != nil {
47 t.Fatalf("SaveToJSON failed: %v", err)
48 }
49
50 // Load
51 newStore := &UserStore{}
52 if err := newStore.LoadFromJSON(filename); err != nil {
53 t.Fatalf("LoadFromJSON failed: %v", err)
54 }
55
56 if len(newStore.Users) != 1 {
57 t.Errorf("Expected 1 user, got %d", len(newStore.Users))
58 }
59}
60
61func TestFindByID(t *testing.T) {
62 store := &UserStore{}
63 store.AddUser(User{ID: 1, Username: "alice"})
64
65 // Should find
66 user, err := store.FindByID(1)
67 if err != nil {
68 t.Errorf("FindByID failed: %v", err)
69 }
70 if user.Username != "alice" {
71 t.Errorf("Expected alice, got %s", user.Username)
72 }
73
74 // Should not find
75 _, err = store.FindByID(999)
76 if err == nil {
77 t.Error("FindByID should fail for non-existent ID")
78 }
79}
Bonus Challenges
- Update/Delete Operations: Add UpdateUser and DeleteUser methods
- JSON Validation: Validate required fields before saving
- Streaming JSON: Handle large JSON files with streaming decoder
- Custom Time Format: Use custom time format in JSON
1type User struct {
2 CreatedAt CustomTime `json:"created_at"`
3}
4
5type CustomTime struct {
6 time.Time
7}
8
9func MarshalJSON() {
10 return json.Marshal(ct.Format("2006-01-02"))
11}
Key Takeaways
- JSON struct tags control marshaling/unmarshaling behavior
- json.Marshal vs MarshalIndent - use Indent for readability
- Handle errors from both marshaling and I/O operations
- Use pointers when you need to modify structs
- omitempty tag excludes zero-value fields from JSON
JSON is the most common data interchange format. Mastering Go's encoding/json package is essential for building modern Go applications that interact with APIs, config files, and data storage.