Working with JSON in Go
Summary: I’ve worked with JSON in various programming languages in context to data exchange/communication between applications. In this article, I’ll give a brief overview of the encoding/json package in Go, and point some gotchas I’ve encountered.
Go to: Encoding | Decoding | HTTP Handler Example | Read and Write JSON to File | Streaming | Gotchas
(JavaScript Object Notation), is a popular data interchange format commonly used for communication between applications.
Working with JSON in Go is stress-free thanks to the package from the standard library.
Encoding and decoding data
To encode or decode data we use the and functions from the json package. Both methods have the following signatures:
func Marshal(v interface{}) ([]byte, error)
func Unmarshal(data []byte, v interface{}) error
Encoding
Here’s a basic example of retrieving the JSON-encoded value of a Config
struct instance:
type Config struct {
BuildDirPath string
MaxRefreshRate int
}
c := Config{BuildDirPath: "/bin"}
b, err := json.Marshal(c)
fmt.Println(string(b), err)
// output:
// {"BuildDirPath":"/bin","MaxRefreshRate":0}
By default, the Marshal
function will include all zero-value exported fields as it treats the field names as the key of the JSON element. The code snippet above elaborates this – the MaxRefreshRate
field had no value set but the JSON-encoded output included the field set to zero.
Custom JSON keys
From the previous code snippet, the JSON-encoded output uses the name of the struct fields. To control this we use the literal – here’s an example:
type Config struct {
BuildDirPath string `json:"build_dir_path"`
MaxRefreshRate string `json:"max_refresh_rate"`
}
Printing the JSON-encoded instance of the update Config
struct will yield the
following:
{"build_dir_path":"/bin","max_refresh_rate":0}
Decoding
To decode JSON data we need to define a data structure (struct or map) where the value will be decoded to. Here’s what it looks like:
configJSON := `{"build_dir_path":"/bin","max_refresh_rate":0}`
// create struct to decode JSON string to
var config Config
err := json.Unmarshal([]byte(configJSON), &config)
// handle err
Destination field identification
Unmarshal
will decode only the fields that it can find in the destination type (config
). In our example, only the build_dir_path
and max_refresh_rate
field of m will be populated, ignoring any other fields not defined in the destination type.
The Unmarshal
function identifies each JSON key on the destination struct by
checking for:
- An exported field with a json tag of
KEY
- An exported field named
KEY
or - An exported field matching
KEY
(case-insensitive)
Decoding an unknown JSON structure
There are cases where you wouldn’t know the structure of the JSON data you’re
decoding, or the data source is subject to changing its structure – telemetry
data for instance. We can decode the JSON data into an interface{}
or
map[string]interface{}
value, and
use type assertions to acess the underlying data structures. Here’s an example:
unknownData := `{"status": "online", "available_items": 200, "stats": [{"p1":
"-2", "p2": "0"}]}`
var data interface{}
_ = json.Unmarshal([]byte(unknownData), &data)
// access underlying data
m := f.(map[string]interface{})
status := m["status"]
fmt.Println(status) // online
HTTP Handler Example
The json package enables encoding and decoding json over HTTP hassle-free. Here’s an example of a HTTP POST request handler.
type BoxhubSync struct {
IP string `json:"ip"`
LastSyncUpdate string `json:"last_sync_update"`
// ... other fields omitted for brevity
}
func readBoxhubSyncRequest(w http.ResponseWriter, r *http.Request){
var reqArgs BoxhubSync
err := decodeReqBody(r, &reqArgs)
if err != nil {
log.Fatal(err)
}
// process reqArgs
// send response to client
resJSON(w, http.StatusCreated, map[string]interface{}{
"status": "success",
"message": "sync requested processed",
})
}
// decodeReqBody decodes from [r.Body] to [v]
func decodeReqBody(r *http.Request, v interface{}) error {
defer r.Body.Close()
b, _ := ioutil.ReadAll(r.Body)
err := json.Unmarshal(b, v)
if err != nil {
return err
}
r.Body = ioutil.NopCloser(bytes.NewBuffer(b))
return nil
}
// resJSON writes a response to the http client
func resJSON(w http.ResponseWriter, code int, payload interface{}) {
res, err := json.Marshal(payload)
if err != nil {
log.Fatal(err)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
w.Write(res)
return
}
JSON Streaming
The json package provides NewEncoder
and NewDecoder
functions to
encode/decode JSON data from/to a stream. This is helpful if your data is coming from an io.Reader
stream (eg. HTTP request body), or you need to decode multiple values from a stream of data.
Encoder
The json.NewEncoder
function returns a new encoder that writes to a
io.Writer
stream:
func NewEncoder(w io.Writer) *Encoder
The Encoder.Encode
function is responsible for writing JSON-encoded data to the
stream. Each time the Encode
function is called, JSON is marshalled from v
and appended to the io.Writer
with a trailing newline.
func (enc *Encoder) Encode(v interface{}) error
Here’s an example of how to encode JSON data to a buffer:
func main() {
buf := new(bytes.Buffer)
config := Config{
BuildDirPath: "/bin",
MaxRefreshRate: 80,
}
err := json.NewEncoder(buf).Encode(u)
if err != nil {
log.Fatal(err)
}
fmt.Print(buf.String())
// output:
{"build_dir_path":"/bin","max_refresh_rate":80}
}
Decoder
The json.NewDecoder
function returns a new decoder that reads from an
io.Reader
.
func NewDecoder(r io.Reader) *Decoder
The Decoder.Decode
function decodes the JSON-encoded data. Decode
reads the next JSON-encoded value from its input and stores it in the value pointed to by v
.
func (dec *Decoder) Decode(v interface{}) error
Here’s an example of how to decode JSON-data from a Stdin stream:
func main() {
decoder := json.NewDecoder(os.Stdin)
output := make(map[string]interface{})
for {
err := decoder.Decode(&output)
if err != nil {
log.Fatal(err)
} else {
fmt.Println("JSON output:")
fmt.Println(output)
output = make(map[string]interface{})
}
}
}
Running the above snippet and passing JSON to Stdin will print the decoded version:
# sample json gotten from: https://jsonapi.org/examples
$ go run stream.go
{
"data": [{
"type": "articles",
"id": "1",
"attributes": {
"title": "JSON:API paints my bikeshed!",
"body": "The shortest article. Ever.",
"created": "2015-05-22T14:56:29.000Z",
"updated": "2015-05-22T14:56:28.000Z"
},
"relationships": {
"author": {
"data": {"id": "42", "type": "people"}
}
}
}],
"included": [
{
"type": "people",
"id": "42",
"attributes": {
"name": "John",
"age": 80,
"gender": "male"
}
}
]
}
JSON out:
map[data:[map[attributes:map[body:The shortest article. Ever. created:2015-05-22T14:56:29.000Z title:JSON:API paints my bikeshed! updated:2015-05-22T14:56:28.000Z] id:1 relationships:map[author:map[data:map[id:42 type:people]]] type:articles]] included:[map[attributes:map[age:80 gender:male name:John] id:42 type:people]]]
Read and Write JSON to file
I’ve needed to read and write json to and from a file. This can come in handy when you have some sort of state stored in files you want to read and write to. Here’s an example of reading and writing a config file:
// using Config struct from previous examples
var config Config
// read file
b, err := io.ReadFile("path/to/config.json")
if err != nil {
log.Fatal(err) // handle err
}
err = json.Unmarshal(b, &config)
if err != nil {
log.Fatal(err)
}
// write file
config.MaxRefreshRate = 300
b, err := json.Marshal(config)
if err != nil {
log.Fatal(err)
}
err = io.WriteFile("path/to/config.json", b, FILE_PERM)
if err != nil {
log.Fatal(err)
}
JSON Gotchas in Go
While working with JSON inputs, I’ve come across things that surprised or confused me, some of which I’ve read about on the encoding/json documentation. I’ll list a few I’ve noted down below:
Unexported fields can’t have data decoded as values
When decoding JSON data to a struct, if there are fields unexported by the struct present in the json blob, the field is skipped:
type User struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
photoURL string `json:"photoURL"`
}
data := `{"first_name": "Jane", "last_name": "Doe", "photoURL": "xx.io/xx"}`
var user User
_ = json.Unmarshal([]byte(data), &user)
fmt.Println(user)
// output:
{Jane Doe }
Nil and empty slices are decoded differently
A JSON-encoded nil slice value is null
, while for an empty slice, is an
empty JSON array:
d := map[string]interface{}{
"nil_slice": []int,
"empty_slice": []int{},
}
// encoding d will output:
{"empty_slice":[],"nil_slice":null}
Maps are sorted alphabetically
Encoding a map to JSON will sort its keys alphabetically:
d := map[string]interface{}{
"status": "success",
"code": 201,
"0": nil,
}
// output:
{"0":null,"code":201,"status":"success"}
[]byte
is encoded as base64 string
When encoding a []byte
to JSON, the value is converted to a base64-encoded string:
d := map[string]interface{}{
"public_key": []byte("public_key"),
...
}
// output:
{"public_key":"cHVibGljX2tleQ=="}
Trailing zeros are removed from floats
When encoding a floating-point number, any trailing zeroes will not appear in the JSON-encoded value:
d := map[string]interface{}{
"starting_balance": 80.0,
"upgrade_perc": 30.23,
...
}
// output:
{"starting_balance":80,"upgrade_perc":30.23}
Decoding numbers to interface{}
resolves to float64
When decoding a JSON number into an interface{}
, the value will have the type float64
:
data := `{"type": "tx", "amount": 230}`
var out map[string]interface{}
_ = json.Unmarshal([]byte(data), &out)
fmt.Printf("%T", out["amount"]) // float64
Wrapping Up
There’s much more to say about the json package and its various applications, but hopefully, this is a helpful introduction. To learn more, I highly recommend the package documentation found .
Let me know your thoughts or feedback, or send ideas for improvements. You can also go the discussions on , , and