JSON encoding nuances
Nil and empty slices are encoded differently
Nil slices in Go will be encoded to the null JSON value, whereas an empty (but not nil) slice will be encoded to an empty JSON array. For example:
var nilSlice []string emptySlice := []string{} m := map[string][]string{ "nilSlice": nilSlice, "emptySlice": emptySlice, }
Will encode to the following JSON:
{"emptySlice":[],"nilSlice":null}
Non-ASCII punctuation characters aren’t supported in struct tags
When using struct tags to change the keys in a JSON object, any tags containing non-ASCII punctuation characters will be ignored. Notably this means that you can’t use en or em dashes, or most currency signs, in struct tags. For example, the following struct:
s := struct { CostUSD string `json:"cost $"` // This is OK. CostEUR string `json:"cost €"` // This contains the non-ASCII punctuation character // € and will be ignored. }{ CostUSD: "100.00", CostEUR: "100.00", }
Will encode to the following JSON (notice that the struct tag renaming the CostEUR key has been ignored):
{"cost $":"100.00","CostEUR":"100.00"}
Integer, time.Time and net.IP values can be used as map keys
It’s possible to encode a map which has integer values as the map keys. The integers will be automatically converted to strings in the resulting JSON (because the keys in a JSON object must always be strings). For example, the following map:
m := map[int]string{ 123: "foo", 456_000: "bar", }
Will encode to the JSON:
{"123":"foo","456000":"bar"}
In addition, map keys that implement the encoding.TextMarshaler interface are also supported. This means that you can also use time.Time and net.IP values as map keys out-of-the-box. For example, the following map:
t1 := time.Now() t2 := t1.Add(24 * time.Hour) m := map[time.Time]string{ t1: "foo", t2: "bar", }
Will encode to JSON which looks similar to this:
{"2009-11-10T23:00:00Z":"foo","2009-11-11T23:00:00Z":"bar"}
Angle brackets and ampersands in strings are escaped
If a string contains the angle brackets < or > these will be escaped to the unicode character codes \u003c and \u003e when encoded to JSON. Likewise the & character will be escaped to \u0026. This is to prevent some web browsers from accidentally interpreting a JSON response as HTML. For example, the following slice:
s := []string{ "<foo>", "bar & baz", }
Will encode to the JSON:
["\u003cfoo\u003e","bar \u0026 baz"]
If you want to prevent these characters from being escaped, you’ll need to use a json.Encoder instance with SetEscapeHTML(false) to perform the encoding.
Trailing zeroes are removed from floats
When encoding a floating-point number with a fractional part that ends in zero(es), any trailing zeroes will not appear in the JSON. For example:
s := []float64{ 123.0, 456.100, 789.990, }
Will be encoded to the JSON:
[123,456.1,789.99]
Working with pre-computed JSON
If you have a string or []byte slice which contains ‘pre-computed’ or ‘pre-encoded’ JSON, by default Go will treat it just like any other string or []byte slice during encoding. That means that a string will be escaped and encoded as a JSON string, and a byte slice will be encoded as a base64 JSON string.
For example, the following struct:
m := struct { Person string }{ Person: `{"name": "Alice", "age": 21}`, }
Will encode to:
{"Person":"{\"name\": \"Alice\", \"age\": 21}"}
If you want to interpolate the pre-computed JSON without any changes, you’ll need to convert the pre-computed JSON value to a json.RawMessage type. Go will then directly interpolate it into the rest of the JSON. For example:
m := struct { Person json.RawMessage }{ Person: json.RawMessage(`{"name": "Alice", "age": 21}`), }
Will encode to the JSON:
{"Person":{"name":"Alice","age":21}}
The MarshalText fallback
If a type doesn’t have a MarshalJSON() method but does have a MarshalText() method instead (so that it implements the encoding.TextMarshaler interface), then Go will fall back to calling this during JSON encoding and present the result as a JSON string.
For example, if you run the following code:
type myFloat float64 func (f myFloat) MarshalText() ([]byte, error) { return []byte(fmt.Sprintf("%.2f", f)), nil } func main() { f := myFloat(1.0/3.0) js, err := json.Marshal(f) if err != nil { log.Fatal(err) } fmt.Printf("%s", js) }
It will print the return value from MarshalText() as a JSON string:
"0.33"
The receiver matters when using MarshalJSON
We touched on this earlier in the book, but it’s really worth emphasizing because it often catches people out. If you implement a MarshalJSON() method on a custom type and the MarshalJSON() method uses a pointer receiver, it will only be used when you are encoding a pointer to the custom type. For any custom type values, it will be completely ignored.
You can see this in action if you run this code on the Go playground.
Unless you specifically want this behavior, I recommend getting into the habit of always using value receivers for a MarshalJSON() method, just like we have in this book.