This article summarizes the problems and solutions I usually encounter in my projects regarding the interconversion between go
language JSON
data and structs.
Basic Serialization
First let’s look at the basic usage of json.Marshal()
(serialization) and json.Unmarshal
(deserialization) in the Go language.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
type Person struct {
Name string
Age int64
Weight float64
}
func main() {
p1 := Person{
Name: "Qimi",
Age: 18,
Weight: 71.5,
}
// struct -> json string
b, err := json.Marshal(p1)
if err != nil {
fmt.Printf("json.Marshal failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b)
// json string -> struct
var p2 Person
err = json.Unmarshal(b, &p2)
if err != nil {
fmt.Printf("json.Unmarshal failed, err:%v\n", err)
return
}
fmt.Printf("p2:%#v\n", p2)
}
|
Output:
1
2
|
str:{"Name":"Qimi","Age":18,"Weight":71.5}
p2:main.Person{Name:"Qimi", Age:18, Weight:71.5}
|
Introduction to the structure tag
The Tag
is the meta-information of the structure, which can be read at runtime through the mechanism of reflection. Tag
is defined after the structure field, wrapped by a pair of backquotes, in the following format.
1
|
`key1:"value1" key2:"value2"`
|
The structure tag
consists of one or more key-value pairs. The keys are separated from the values using colons and the values are enclosed in double quotes. Multiple key-value pairs tag
can be set for the same structure field, with spaces separating the different key-value pairs.
Specify the field name using json tag
Serialization and deserialization use the field name of the structure by default, we can specify the field name generated by json
serialization by adding tag
to the structure field.
1
2
3
4
5
6
|
// Use json tag to specify the behavior when serializing and deserializing
type Person struct {
Name string `json:"name"` // Specify lowercase name forand deserializing json
Age int64
Weight float64
}
|
Ignore a field
If you want to ignore a field in the structure when serializing/deserializing json, you can add -
to the tag
as follows.
1
2
3
4
5
6
|
// Use json tag to specify the behavior of json when serializing and deserializing
type Person struct {
Name string `json:"name"` // Specify lowercase name for json serialization/deserialization
Age int64
Weight float64 `json:"-"` // Specify to ignore this field when serializing/deserializing json
}
|
Ignore nil fields
Marshal()
does not ignore fields in struct
when serializing them without a value, but outputs the type zero of the field by default (e.g. int
and float
type zero is 0
, string type zero is “”, object type zero is nil
). If you want to ignore these fields that have no value when serializing, you can add omitempty tag
to the corresponding field.
As an example.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Hobby []string `json:"hobby"`
}
func omitemptyDemo() {
u1 := User{
Name: "Qimi",
}
// struct -> json string
b, err := json.Marshal(u1)
if err != nil {
fmt.Printf("json.Marshal failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b)
}
|
Output:
1
|
str:{"name":"Qimi","email":"","hobby":null}
|
If you want to remove the null field from the final serialization result, you can define the structure like the following.
1
2
3
4
5
6
7
|
// Add omitempty to tag to ignore null values
// Note that here hobby,omitempty together are json tag values, separated by English commas
type User struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
Hobby []string `json:"hobby,omitempty"`
}
|
At this point, execute the above omitemptyDemo
again and the output will be as follows.
1
|
str:{"name":"Qimi"} // No email and hobby fields in serialization results
|
Ignore nested structure nil fields
First, look at several examples of structure nesting.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
type User struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
Hobby []string `json:"hobby,omitempty"`
Profile
}
type Profile struct {
Website string `json:"site"`
Slogan string `json:"slogan"`
}
func nestedStructDemo() {
u1 := User{
Name: "Qimi",
Hobby: []string{"Soccer", "Two Color Ball"},
}
b, err := json.Marshal(u1)
if err != nil {
fmt.Printf("json.Marshal failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b)
}
|
The serialized json
string when nesting Profile
anonymously is a single level of.
1
|
str:{"name":"Qimi","hobby":["Soccer","Two Color Ball"],"site":"","slogan":""}
|
To turn it into a nested json
string, you need to change it to a named nest or define the field tag
.
1
2
3
4
5
6
7
|
type User struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
Hobby []string `json:"hobby,omitempty"`
Profile `json:"profile"`
}
// str:{"name":"Qimi","hobby":["Soccer","Two Color Ball"],"profile":{"site":"","slogan":""}}
|
It is not enough to add omitempty
if you want to ignore the field when the nested structure has a null value.
1
2
3
4
5
6
7
|
type User struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
Hobby []string `json:"hobby,omitempty"`
Profile `json:"profile,omitempty"`
}
// str:{"name":"Qimi","hobby":["Soccer","Two Color Ball"],"profile":{"site":"","slogan":""}}
|
It is also necessary to use nested structure pointers.
1
2
3
4
5
6
7
|
type User struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
Hobby []string `json:"hobby,omitempty"`
*Profile `json:"profile,omitempty"`
}
// str:{"name":"Qimi","hobby":["Soccer","Two Color Ball"]}
|
Ignore nil fields without modifying the original structure
We need json
to serialize User
, but do not want to serialize the password as well, and do not want to modify the User
structure, this time we can use to create another structure PublicUser
anonymously nested in the original User
, while specifying the Password
field as an anonymous structure pointer type, and add omitemptytag
, sample code as follows.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
type User struct {
Name string `json:"name"`
Password string `json:"password"`
}
type PublicUser struct {
*User // Anonymous nesting
Password *struct{} `json:"password,omitempty"`
}
func omitPasswordDemo() {
u1 := User{
Name: "Qimi",
Password: "123456",
}
b, err := json.Marshal(PublicUser{User: &u1})
if err != nil {
fmt.Printf("json.Marshal u1 failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b) // str:{"name":"Qimi"}
}
|
Sometimes the front-end may use numbers of string type in the passed json
data, in this case you can add string
to the structure tag
to tell the json
package to parse the data of the corresponding field from the string.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
type Card struct {
ID int64 `json:"id,string"` // add string tag
Score float64 `json:"score,string"` // add string tag
}
func intAndStringDemo() {
jsonStr1 := `{"id": "1234567","score": "88.50"}`
var c1 Card
if err := json.Unmarshal([]byte(jsonStr1), &c1); err != nil {
fmt.Printf("json.Unmarsha jsonStr1 failed, err:%v\n", err)
return
}
fmt.Printf("c1:%#v\n", c1) // c1:main.Card{ID:1234567, Score:88.5}
}
|
Integer to floating
In the JSON
protocol there is no distinction between integer and floating point types, they are collectively called number
. Numbers in json
strings are deserialized by the json
package in the Go
language and become float64
types. The following code demonstrates this problem.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
func jsonDemo() {
// map[string]interface{} -> json string
var m = make(map[string]interface{}, 1)
m["count"] = 1 // int
b, err := json.Marshal(m)
if err != nil {
fmt.Printf("marshal failed, err:%v\n", err)
}
fmt.Printf("str:%#v\n", string(b))
// json string -> map[string]interface{}
var m2 map[string]interface{}
err = json.Unmarshal(b, &m2)
if err != nil {
fmt.Printf("unmarshal failed, err:%v\n", err)
return
}
fmt.Printf("value:%v\n", m2["count"]) // 1
fmt.Printf("type:%T\n", m2["count"]) // float64
}
|
This scenario requires the use of decoder
to deserialize the numbers if you want to handle them more rationally, and the sample code is as follows.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
func decoderDemo() {
// map[string]interface{} -> json string
var m = make(map[string]interface{}, 1)
m["count"] = 1 // int
b, err := json.Marshal(m)
if err != nil {
fmt.Printf("marshal failed, err:%v\n", err)
}
fmt.Printf("str:%#v\n", string(b))
// json string -> map[string]interface{}
var m2 map[string]interface{}
// Deserialize using the `decoder` method, specifying the use of the `number` type
decoder := json.NewDecoder(bytes.NewReader(b))
decoder.UseNumber()
err = decoder.Decode(&m2)
if err != nil {
fmt.Printf("unmarshal failed, err:%v\n", err)
return
}
fmt.Printf("value:%v\n", m2["count"]) // 1
fmt.Printf("type:%T\n", m2["count"]) // json.Number
// After converting m2["count"] to json.Number, call the Int64() method to get a value of type int64
count, err := m2["count"].(json.Number).Int64()
if err != nil {
fmt.Printf("parse to int64 failed, err:%v\n", err)
return
}
fmt.Printf("type:%T\n", int(count)) // int
}
|
The source code of json.Number
is defined as follows.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// A Number represents a JSON number literal.
type Number string
// String returns the literal text of the number.
func (n Number) String() string { return string(n) }
// Float64 returns the number as a float64.
func (n Number) Float64() (float64, error) {
return strconv.ParseFloat(string(n), 64)
}
// Int64 returns the number as an int64.
func (n Number) Int64() (int64, error) {
return strconv.ParseInt(string(n), 10, 64)
}
|
We need to get the json.Number
type first when dealing with number
type json.Number
fields, and then call Float64()
or Int64()
depending on the actual type of the field.
Customized parsing time field
The Go
language’s built-in json
package uses the time format defined in the RFC3339
standard, which has many restrictions on how we can serialize time fields.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
type Post struct {
CreateTime time.Time `json:"create_time"`
}
func timeFieldDemo() {
p1 := Post{CreateTime: time.Now()}
b, err := json.Marshal(p1)
if err != nil {
fmt.Printf("json.Marshal p1 failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b)
jsonStr := `{"create_time":"2020-04-05 12:25:42"}`
var p2 Post
if err := json.Unmarshal([]byte(jsonStr), &p2); err != nil {
fmt.Printf("json.Unmarshal failed, err:%v\n", err)
return
}
fmt.Printf("p2:%#v\n", p2)
}
|
The output of the above code is as follows
1
2
|
str:{"create_time":"2020-04-05T12:28:06.799214+08:00"}
json.Unmarshal failed, err:parsing time ""2020-04-05 12:25:42"" as ""2006-01-02T15:04:05Z07:00"": cannot parse " 12:25:42"" as "T"
|
That is, the built-in json
package does not recognize our common string time format, such as 2020-04-05 12:25:42
.
However, we implement custom event format parsing by implementing the json.Marshaler/json.Unmarshaler
interface.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
type CustomTime struct {
time.Time
}
const ctLayout = "2006-01-02 15:04:05"
var nilTime = (time.Time{}).UnixNano()
func (ct *CustomTime) UnmarshalJSON(b []byte) (err error) {
s := strings.Trim(string(b), "\"")
if s == "null" {
ct.Time = time.Time{}
return
}
ct.Time, err = time.Parse(ctLayout, s)
return
}
func (ct *CustomTime) MarshalJSON() ([]byte, error) {
if ct.Time.UnixNano() == nilTime {
return []byte("null"), nil
}
return []byte(fmt.Sprintf("\"%s\"", ct.Time.Format(ctLayout))), nil
}
func (ct *CustomTime) IsSet() bool {
return ct.UnixNano() != nilTime
}
type Post struct {
CreateTime CustomTime `json:"create_time"`
}
func timeFieldDemo() {
p1 := Post{CreateTime: CustomTime{time.Now()}}
b, err := json.Marshal(p1)
if err != nil {
fmt.Printf("json.Marshal p1 failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b)
jsonStr := `{"create_time":"2020-04-05 12:25:42"}`
var p2 Post
if err := json.Unmarshal([]byte(jsonStr), &p2); err != nil {
fmt.Printf("json.Unmarshal failed, err:%v\n", err)
return
}
fmt.Printf("p2:%#v\n", p2)
}
|
Custom MarshalJSON and UnmarshalJSON methods
The above method of customizing types is a little bit more verbose, so here is a relatively convenient way to look at it.
The first thing you need to know is that if you can implement the MarshalJSON()([]byte, error)
and UnmarshalJSON(b []byte) error
methods for a type, then that type will use your custom methods when serializing (MarshalJSON)/deserializing (UnmarshalJSON).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
type Order struct {
ID int `json:"id"`
Title string `json:"title"`
CreatedTime time.Time `json:"created_time"`
}
const layout = "2006-01-02 15:04:05"
// MarshalJSON implements custom MarshalJSON methods for Order types
func (o *Order) MarshalJSON() ([]byte, error) {
type TempOrder Order // Define a new type consistent with the Order field
return json.Marshal(struct {
CreatedTime string `json:"created_time"`
*TempOrder // Avoid nesting Order directly into a dead loop
}{
CreatedTime: o.CreatedTime.Format(layout),
TempOrder: (*TempOrder)(o),
})
}
// UnmarshalJSON Implement custom UnmarshalJSON methods for Order types
func (o *Order) UnmarshalJSON(data []byte) error {
type TempOrder Order // Define a new type consistent with the Order field
ot := struct {
CreatedTime string `json:"created_time"`
*TempOrder // Avoid nesting Order directly into a dead loop
}{
TempOrder: (*TempOrder)(o),
}
if err := json.Unmarshal(data, &ot); err != nil {
return err
}
var err error
o.CreatedTime, err = time.Parse(layout, ot.CreatedTime)
if err != nil {
return err
}
return nil
}
// Custom Serialization Methods
func customMethodDemo() {
o1 := Order{
ID: 123456,
Title: "《Qimi's Go Learning Notes》",
CreatedTime: time.Now(),
}
// struct -> json string via custom MarshalJSON method
b, err := json.Marshal(&o1)
if err != nil {
fmt.Printf("json.Marshal o1 failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b)
// json string -> struct via custom UnmarshalJSON method
jsonStr := `{"created_time":"2020-04-05 10:18:20","id":123456,"title":"《Qimi's Go Learning Notes》"}`
var o2 Order
if err := json.Unmarshal([]byte(jsonStr), &o2); err != nil {
fmt.Printf("json.Unmarshal failed, err:%v\n", err)
return
}
fmt.Printf("o2:%#v\n", o2)
}
|
Output results.
1
2
|
str:{"created_time":"2020-04-05 10:32:20","id":123456,"title":"《Qimi's Go Learning Notes》"}
o2:main.Order{ID:123456, Title:"《Qimi's Go Learning Notes》", CreatedTime:time.Time{wall:0x0, ext:63721678700, loc:(*time.Location)(nil)}}
|
Adding fields using anonymous structs
The use of inline structs can extend the fields of a struct, but sometimes it is not necessary to define new structs separately and the operation can be simplified by using anonymous structs.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
type UserInfo struct {
ID int `json:"id"`
Name string `json:"name"`
}
func anonymousStructDemo() {
u1 := UserInfo{
ID: 123456,
Name: "Qimi",
}
// Use anonymous structure with embedded User and add additional field Token
b, err := json.Marshal(struct {
*UserInfo
Token string `json:"token"`
}{
&u1,
"91je3a4s72d1da96h",
})
if err != nil {
fmt.Printf("json.Marsha failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b)
// str:{"id":123456,"name":"Qimi","token":"91je3a4s72d1da96h"}
}
|
Combining Multiple Structs with Anonymous Structs
Similarly, anonymous structures can be used to combine multiple structures to serialize and deserialize data: the
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
type Comment struct {
Content string
}
type Image struct {
Title string `json:"title"`
URL string `json:"url"`
}
func anonymousStructDemo2() {
c1 := Comment{
Content: "Never overestimate yourself",
}
i1 := Image{
Title: "QR Code",
URL: "https://www.foo.com/qr.png",
}
// struct -> json string
b, err := json.Marshal(struct {
*Comment
*Image
}{&c1, &i1})
if err != nil {
fmt.Printf("json.Marshal failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b)
// json string -> struct
jsonStr := `{"Content":"Never overestimate yourself","title":"QR Code","url":"https://www.foo.com/qr.png"}`
var (
c2 Comment
i2 Image
)
if err := json.Unmarshal([]byte(jsonStr), &struct {
*Comment
*Image
}{&c2, &i2}); err != nil {
fmt.Printf("json.Unmarshal failed, err:%v\n", err)
return
}
fmt.Printf("c2:%#v i2:%#v\n", c2, i2)
}
|
Output:
1
2
|
str:{"Content":"Never overestimate yourself","title":"QR Code","url":"https://www.foo.com/qr.png"}
c2:main.Comment{Content:"Never overestimate yourself"} i2:main.Image{Title:"QR Code", URL:"https://www.foo.com/qr.png"}
|
Handling json with uncertain hierarchy
If the json string does not have a fixed format that makes it difficult to define its corresponding structure, we can use json.RawMessage
to save the raw byte data.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
type sendMsg struct {
User string `json:"user"`
Msg string `json:"msg"`
}
func rawMessageDemo() {
jsonStr := `{"sendMsg":{"user":"q1mi","msg":"Never overestimate yourself"},"say":"Hello"}`
// Define a map with value type json.RawMessage for more flexible processing
var data map[string]json.RawMessage
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
fmt.Printf("json.Unmarshal jsonStr failed, err:%v\n", err)
return
}
var msg sendMsg
if err := json.Unmarshal(data["sendMsg"], &msg); err != nil {
fmt.Printf("json.Unmarshal failed, err:%v\n", err)
return
}
fmt.Printf("msg:%#v\n", msg)
// msg:main.sendMsg{User:"q1mi", Msg:"Never overestimate yourself"}
}
|
Reference https://www.liwenzhou.com/posts/Go/json_tricks_in_go/