Jida's Blog

Golang Standard Package: encoding/json

21st November 2024
Golang
Golang Standard Package
JSON
Last updated:25th January 2025
4 Minutes
715 Words

1. Intro & How to use

There are two main functions in the encoing/json package: Marshal, and Unmarshal:

  • Marshal(v any) ([]byte, error): encode Golang object into JSON.
  • Unmarshal(data []byte, v any) error: decode JSON to Golang object.

Here, the Golang object refers to both struct and slice, map.

1.1 Marshal

1
package main
2
3
import (
4
"encoding/json"
5
"fmt"
6
)
7
8
type Employee struct {
9
Name string `json:"name"`
10
Position string `json:"position"`
11
Age int `json:"age"`
12
Salary uint64 `json:"salary"`
13
}
14
15
func main() {
17 collapsed lines
16
// 1. Golang object -> JSON
17
employee1 := Employee{
18
Name: "Alice",
19
Position: "Manager",
20
Age: 30,
21
Salary: 5000,
22
}
23
jsonData, err := json.Marshal(employee1)
24
if err != nil {
25
fmt.Println("Error marshaling JSON:", err)
26
return
27
}
28
29
fmt.Println(string(jsonData))
30
}
31
// output:
32
// {"name":"Alice","position":"Manager","age":30,"salary":5000}

1.2 Unmarshal

1
func main() {
2
// 2. JSON -> Golang object
3
jsonStr := `{"name":"Bob","position":"Developer","age":25,"salary":4000000}`
4
var employee2 Employee
5
err = json.Unmarshal([]byte(jsonStr), &employee2)
6
if err != nil {
7
fmt.Println("Error unmarshaling JSON:", err)
8
return
9
}
10
fmt.Println(employee2)
11
}
12
// output:
13
// {Bob Developer 25 4000000}

1.3 JSON tag options

  • json:"key": Specify the key in the JSON output.
  • json:"key,omitempty": Omit the field if it is empty (i.e., zero value).
  • json:"-": Ignore the field (do not marshal or unmarshal it).

2. Attentions!!!

1) pubic fields

When using a struct to handle JSON, the strut’s fields must be public, i.e. the field names must start with an uppercase letter.

  • Example:

    1
    type Employee struct {
    2
    Name string
    3
    Position string
    4
    Age int
    5
    **salary uint64 // private field**
    6
    }
    7
    func main() {
    8
    jsonStr := `{"name":"Bob","position":"Developer","age":25,"salary":4000000}`
    9
    var employee2 Employee
    10
    err = json.Unmarshal([]byte(jsonStr), &employee2)
    11
    if err != nil {
    12
    fmt.Println("Error unmarshaling JSON:", err)
    13
    return
    14
    }
    15
    fmt.Println(employee2)
    4 collapsed lines
    16
    }
    17
    // when unmarshaling, we cannot read correct value of `salary`
    18
    // output:
    19
    // {Bob Developer 25 **0**}

[!IMPORTANT] In Go, the language design prohibits reflection from accessing private struct fields. This is done intentionally to enforce the principles of encapsulation and data privacy.

2) avoid using map

Avoid using map, because map brings extra cost, extrac code, and extra maintenance cost.

Why consider struct before map: struct defines the fields and their types ahead, while map has no constraints to data → map cannot constraint the change of JSON, bring extra work and mantenance cost.

1
func main() {
2
var m map[string]any
3
err = json.Unmarshal([]byte(jsonStr), &m)
4
if err != nil {
5
fmt.Println("Error unmarshaling JSON:", err)
6
return
7
}
8
// lots of codes for maintaining the map
9
name, ok := m["name"].(string)
10
if !ok {
11
fmt.Println("Error type assertion")
12
return
13
}
14
fmt.Println(name)
15
// extra work for other fields...
1 collapsed line
16
}

3) repeat unmarshaling

Dirty data contamination: the fields that were not explicitly overwritten in the second JSON data retain their values from the previous unmarshalling

Solution: whenever unmarshaling JSON, use a new struct object to load the data

1
func main() {
2
// First JSON string with the salary field
3
jsonStr3 := `{"name":"Bob","position":"Developer","age":25,"salary":4000000}`
4
var employee Employee
5
_ = json.Unmarshal([]byte(jsonStr3), &employee)
6
fmt.Println("employee3:", employee)
7
// Second JSON string without the salary field
8
jsonStr4 := `{"name":"Kate","position":"Senior Developer","age":35}`
9
// Salary remains 4000000
10
_ = json.Unmarshal([]byte(jsonStr4), &employee)
11
fmt.Println("employee4:", employee)
12
}
13
// outputs:
14
// employee3: {Bob Developer 25 4000000}
15
// employee4: {Kate Senior Developer 35 **4000000**}

4) confusion brought by default value

In Golang, the default value of int is 0, string’s is “”, pointer’s is nil, etc.

Issue: with the approach above, we cannot distinguish the difference between missing object, or the case that value is indeed default value.

Solution: by changing the field to pointer, we can use pointer’s nil to distinguish the difference.

1
func ZeroValueConfusion() {
2
str := `{"name":"Bob","position":"Developer","age":25,"salary":100}`
3
var p Employee
4
_ = json.Unmarshal([]byte(str), &p)
5
fmt.Printf("ZeroValueConfusion: %+v\n", p)
6
7
str2 := `{"name":"Bob","position":"Developer","age":25}`
8
var p2 Employee
9
_ = json.Unmarshal([]byte(str2), &p2)
10
fmt.Printf("ZeroValueConfusion: %+v\n", p2)
11
if p2.Salary == nil {
12
fmt.Println("Salary is nil")
13
}
14
}
15
4 collapsed lines
16
// outputs:
17
// {Name:Bob Position:Developer Age:25 Salary:0x1400000e2f0}
18
// {Name:Bob Position:Developer Age:25 Salary:<nil>}
19
// p2.Salary is nil

5) JSON tags

  • Confusion brought by default value: default value or missing value
1
type Employee struct {
2
Name string `json:"name"`
3
Position string `json:"position"`
4
Age int `json:"age,omitempty"`
5
Salary uint64 `json:"salary,omitempty"`
6
}
7
8
func TagMarshal() {
9
p := Employee{
10
Name: "Bob",
11
Salary: 0,
12
}
13
output, _ := json.MarshalIndent(p, "", " ")
14
println(string(output))
15
}
5 collapsed lines
16
// outputs:
17
// {
18
// "name": "Bob",
19
// "position": ""
20
// }
  • Solution: use pointer

    1
    type Employee struct {
    2
    Name string `json:"name"`
    3
    Position string `json:"position"`
    4
    Age int `json:"age,omitempty"`
    5
    Salary *uint64 `json:"salary,omitempty"`
    6
    }
    7
    8
    func TagMarshal() {
    9
    p := Employee{
    10
    Name: "Bob",
    11
    Salary: new(uint64),
    12
    }
    13
    *p.Salary = 100
    14
    output, _ := json.MarshalIndent(p, "", " ")
    15
    println(string(output))
    8 collapsed lines
    16
    }
    17
    18
    // outputs:
    19
    // {
    20
    // "name": "Bob",
    21
    // "position": "",
    22
    // "salary": 100
    23
    // }

You may find the full example of the code here: https://github.com/jidalii/go-playground/tree/main/parse-json

References

Article title:Golang Standard Package: encoding/json
Article author:Jida-Li
Release time:21st November 2024