Working with JSON in strictly-typed languages (such as Go) can be tricky. That is, I'm talking about the conversion from JSON to natively defined structures and visa-versa so that you never have to deal with manipulating JSON through generic arrays and dictionaries which generates a lot of verbose type checking and runtime type casting. Go actually does a remarkable job at this without much effort - like most things in Go.

Go provides native libraries for JSON and it integrates well with the language. There is a lot of great documentation in the encoding/json package.

I will try and cover the 90% cases in this article.

All examples will use the following basic imports so that the same lines don't need to be repeated:

import (
"encoding/json"
"fmt"
)

Decoding JSON

In a perfect world the schema of the object and the struct definition would both remain the same and have predicable value types. You could say this is easiest scenario, it's also the most ideal place to start.

json.Unmarshal takes a byte array (we convert a string to []byte) and a reference to the object you wish to decode the value into.

type Person struct {
FirstName string
LastName string
}

func main() {
theJson := `{"FirstName": "Bob", "LastName": "Smith"}`

var person Person
json.Unmarshal([]byte(theJson), &person)

fmt.Printf("%+v\n", person)
}

{FirstName:Bob LastName:Smith}



Go uses the case of the first letter to designate whether the entity is exported or not. However, this does not effect the decoding because it is not sensitive to case. For example {"firstname": "Bob", "lastname": "Smith"} decoded into:

type Person struct {
FirstName string
LastName string
}

Will still map the keys correctly. Just be careful if you need to encode this data back to JSON the keys will change case which will potentially break other systems. There is a simple way around this with tags explained later.

Encoding JSON

Encoding JSON is very easy. We simply provide the variable to be encoded to the json.Marshal function:

type Person struct {
FirstName string
LastName string
}

func main() {
person := Person{"James", "Bond"}
theJson, _ := json.Marshal(person)

fmt.Printf("%+v\n", string(theJson))
}

{"FirstName":"James","LastName":"Bond"}

Mapping Keys

If you need to map completely different JSON keys to a struct you can use tags. Tags are text-based annotations added to variables:

type Person struct {
FirstName string `json:"fn"`
LastName string `json:"ln"`
}

func main() {
theJson := `{"fn": "Bob", "ln": "Smith"}`

var person Person
json.Unmarshal([]byte(theJson), &person)

fmt.Printf("%+v\n", person)
}

This also applies to encoding the object so that the keys will be mapped back to their original names.

Default Values

The json.Unmarshal function takes in an object rather than a type. Any values decoded in the JSON will replace values found in the object, but otherwise it will leave the values as they were which is ideal for setting up default properties for your object:

type Person struct {
FirstName string
LastName string
}

func newPerson() Person {
return Person{"", ""}
}

func main() {
theJson := `{"FirstName": "Bob"}`
person := newPerson()
json.Unmarshal([]byte(theJson), &person)

fmt.Printf("%+v\n", person)
}

{FirstName:Bob LastName:}

Handling Errors

If the JSON is unable to be parsed (like a syntax error) the error is returned from the json.Unmarshal function:

func main() {
theJson := `invalid json`
err := json.Unmarshal([]byte(theJson), nil)

fmt.Printf("%+v\n", err)
}

invalid character 'i' looking for beginning of value

More commonly is that the JSON values are of a different type than those defined in the Go structures and variables. Here is an example of trying to encode a JSON string into an integer variable:

func main() {
theJson := `"123"`

var value int
err := json.Unmarshal([]byte(theJson), &value)

fmt.Printf("%+v\n", err)
}

json: cannot unmarshal string into Go value of type int

How you handle these errors is up to you, but if your dealing with JSON that can be messy or dynamic you need to keep reading...

Dynamic JSON

Dynamic JSON is the most difficult to deal with because you have to investigate the type of data before you can reliably use it.

There are two approaches to dealing with dynamic JSON:

1. Create a flexible skeleton to unserialise into and test the types inside that.

type PersonFlexible struct {
Name interface{}
}

type Person struct {
Name string
}

func main() {
theJson := `{"Name": 123}`

var personFlexible PersonFlexible
json.Unmarshal([]byte(theJson), &personFlexible)

if _, ok := personFlexible.Name.(string); !ok {
panic("Name must be a string.")
}

// When validation passes we can use the real object and types.
// This code will never be reached because the above will panic()...
// But if you make the Name above a string it will run the following:
var person Person
json.Unmarshal([]byte(theJson), &person)

fmt.Printf("%+v\n", person)
}

2. Go totally rogue (pun intended) and investigate the entire value one step at a time.

This is akin to dealing with JSON in JavaScript with lots of typeof conditions. You can do the same thing in Go like above or use the beauty of the switch:

func main() {
theJson := `123`

var anything interface{}
json.Unmarshal([]byte(theJson), &anything)

switch v := anything.(type) {
case float64:
// v is an float64
fmt.Printf("NUMBER: %f\n", v)

case string:
// v is a string
fmt.Printf("STRING: %s\n", v)

default:
panic("I don't know how to handle this!")
}
}

Important: Go uses fixed types for JSON values. Even though you would think 123 would be decoded as some type of int it will actually always be a float64. This simplifies the type switches but may require you to also round or test for an actual integer value if that's a requirement.

Validating JSON Schemas

If you have a complex JSON structure and/or more complex rules about how that data should be formatted it is easier to use the JSON Schema format to do the validation for you.

I won't go into too much detail here because JSON Schema options could fill more than a whole article. However, here is an quick example using the xeipuuv/gojsonschema package with the example they provide:

import (
"fmt"
"github.com/xeipuuv/gojsonschema"
)

func main() {
schemaLoader := gojsonschema.NewReferenceLoader("file:///home/me/schema.json")
documentLoader := gojsonschema.NewReferenceLoader("file:///home/me/document.json")

result, err := gojsonschema.Validate(schemaLoader, documentLoader)
if err != nil {
panic(err.Error())
}

if result.Valid() {
fmt.Printf("The document is valid\n")
} else {
fmt.Printf("The document is not valid. see errors :\n")
for _, desc := range result.Errors() {
fmt.Printf("- %s\n", desc)
}
}
}