During the development process using the Go language, the json package is often used for mutual conversion between JSON and structs. In the process, I encountered some areas that require extra attention, which I have documented below.

Integer to Float Conversion Issue

Suppose there is a Person structure that contains two fields: Age int64 and Weight float64. Now, let’s convert the Person structure to map[string]interface{} using the json package. The code is as follows:

type Person struct {
	Name string
	Age int64
	Weight float64
}

func main() {
    person := Person{
        Name: "Wang Wu",
        Age: 30,
        Weight: 150.07,
    }

    jsonBytes, _ := json.Marshal(person)
    fmt.Println(string(jsonBytes))

    var personFromJSON interface{}
    json.Unmarshal(jsonBytes, &personFromJSON)

    r := personFromJSON.(map[string]interface{})
}

At this point, everything seems normal, but printing the map[string]interface{} reveals something unusual.

fmt.Println(reflect.TypeOf(r["Age"]).Name())  // float64
fmt.Println(reflect.TypeOf(r["Weight"]).Name())  // float64

After converting to map[string]interface{}, the original int64 and float64 types have both been converted to float64, which is clearly not what we expected.

Upon reviewing the JSON specification, we see that there is no distinction between integer and floating-point types in JSON. Therefore, we can understand why the Unmarshal method in the json package converts number types to float64. According to the JSON specification, numbers are of the same type, and the closest corresponding Go type is float64.

The json package provides a better solution for this issue, but it requires using json.Decoder instead of the json.Unmarshal method. Replace json.Unmarshal as follows:

var personFromJSON interface{}

decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
decoder.UseNumber()
decoder.Decode(&personFromJSON)

r := personFromJSON.(map[string]interface{})

This method first creates a jsonDecoder and then calls the UseNumber method. According to the documentation, using the UseNumber method causes the json package to convert numbers into a built-in Number type (instead of float64). This Number type provides methods for conversion to int64, float64, and others.

Time Format

In JSON format, there is no time type. When storing dates and times in JSON format, they need to be converted to string type. This brings up a problem: there are various string representations of date and time. Which one does the Go json package support?

Use the following code to output the format after the json.Marshal method converts the Time type to a string.

type Person struct {
	Name string
	Birth time.Time
}

func main() {
	person := Person{
		Name: "Wang Wu",
		Birth: time.Now(),
	}

	jsonBytes, _ := json.Marshal(person)
	fmt.Println(string(jsonBytes))  // {"Name":"Wang Wu","Birth":"2018-12-20T16:22:02.00287617+08:00"}
}

Based on the output, we can determine that the Go json package uses the format defined in the RFC3339 standard. Next, let’s test which date and time formats are supported by the json.Unmarshal method.

dateStr := "2018-10-12"

var person Person
jsonStr := fmt.Sprintf("{\"name\":\"Wang Wu\", \"Birth\": \"%s\"}", dateStr)
json.Unmarshal([]byte(jsonStr), &person)

fmt.Println(person.Birth)  // 0001-01-01 00:00:00 +0000 UTC

For strings like “2018-10-12”, the json package did not successfully parse them. Next, let’s try all the formats supported by the time package.

After testing, we found that the json.Unmarshal method only supports conversion of RFC3339 and RFC3339Nano formats. Another point to note is that the time generated using time.Now() includes a Monotonic Time. During the json.Marshal conversion, since there is no place to store Monotonic Time in the RFC3339 specification, this part will be lost.

Handling Empty Fields

Handling empty values is an area where the json package can easily lead to errors. Consider the following code:

type Person struct {
	Name     string
	Age      int64
	Birth    time.Time
	Children []Person
}

func main() {
	person := Person{}

	jsonBytes, _ := json.Marshal(person)
	fmt.Println(string(jsonBytes))  // {"Name":"","Age":0,"Birth":"0001-01-01T00:00:00Z","Children":null}
}

When the fields in a struct have no values, using the json.Marshal method does not automatically ignore these fields. Instead, it outputs their default empty values based on the field types, which often does not align with our expectations. The json package provides control over fields, allowing us to add the omitempty tag to fields. This tag will ignore the field when its value is zero (the zero value for int and float types is 0, for string types is “”, and for object types is nil).

type PersonAllowEmpty struct {
	Name     string             `json:",omitempty"`
	Age      int64              `json:",omitempty"`
	Birth    time.Time          `json:",omitempty"`
	Children []PersonAllowEmpty `json:",omitempty"`
}

func main() {
	person := PersonAllowEmpty{}
	jsonBytes, _ := json.Marshal(person)
	fmt.Println(string(jsonBytes))  // {"Birth":"0001-01-01T00:00:00Z"}
}

As you can see, this time the output JSON only contains the Birth field. The fields of string, int, and object types were ignored because they were not assigned values and defaulted to zero values. For date and time types, since they cannot be set to zero value (i.e., 0000-00-00 00:00:00), they are not ignored.

Be cautious of this situation: if a person’s age is 0 (which is reasonable for a newborn), it coincides with the zero value for the int field. With the omitempty tag added, the age field will be ignored.

If you want a specific field to be ignored by the json package under any circumstances, use the following syntax:

type Person struct {
	Name     string `json:"-"`
	Age      int64 `json:"-"`
	Birth    time.Time `json:"-"`
	Children []string `json:"-"`
}

func main() {
    birth, _ := time.Parse(time.RFC3339, "1988-12-02T15:04:27+08:00")
	person := Person{
		Name: "Wang Wu",
		Age: 30,
		Birth: birth,
		Children: []string{},
	}

	jsonBytes, _ := json.Marshal(person)
    fmt.Println(string(jsonBytes))  // {}
}

As you can see, fields with the json:"-" tag are all ignored.