Advanced JSON handling in Go 19:40 05 Mar 2020 Jonathan Hall DevOps Evangelist / Go Developer / Clean Coder / Salsa Dancer About me

Open Source contributor; CouchDB PMC, author of Kivik Core Tech Lead for Lana Former eCommerce Dev Manager at Bugaboo Former backend developer at Teamwork.com Former backend developer at Booking.com

Former tech lead at eFolder/DoubleCheck 2 Show of hands

Who has...

...used JSON in a Go program? ...been frustrated by Go's strict typing when dealing with JSON? ...felt limited by Go's standard JSON handling?

What have been your biggest frustrations? 3 Today's Topics

Very brief intro to JSON in Go Basic use of maps and structs Handling inputs of unknown type

Handling data with some unknown fields 4 A brief intro to JSON

JavaScript Object Notation, defined by RFC 8259 Human-readable, textual representation of arbitrary data Limted types: null, Number, String, Boolean, Array, Object

Broad applications: Config files, data interchange, simple messaging 5 Alternatives to JSON

YAML, TOML, INI BSON, MessagePack, CBOR, Smile XML ProtoBuf Custom/proprietary formats

Many principles discussed in this presentation apply to any of the above formats. 6 Marshaling JSON

Creating JSON from a Go object is (usually) very straight forward:

func main() { x := map[string]string{ "foo": "bar", } data, _ := .Marshal(x) fmt.Println(string(data)) } Run

7 Marshaling JSON, #2

Creating JSON from a Go object is (usually) very straight forward:

func main() { type person struct { Name string `json:"name"` Age int `json:"age"` Description string `json:"descr,omitempty"` secret string // Unexported fields are never (un)marshaled } x := person{ Name: "Bob", Age: 32, secret: "Shhh!", } data, _ := json.Marshal(x) fmt.Println(string(data)) } Run

8 Unmarshaling JSON

Unmarshaling JSON is often a bit trickier.

func main() { data := []byte(`{"foo":"bar"}`) var x interface{} _ = json.Unmarshal(data, &x) spew.Dump(x) } Run

9 Unmarshaling JSON, #2

Avoid a map whenever possible. interface{} says nothing (https://www.youtube.com/watch?v=PAAkCSZUG1c&t=7m36s)

func main() { type person struct { Name string `json:"name"` Age int `json:"age"` Description string `json:"descr,omitempty"` secret string // Unexported fields are never (un)marshaled } data := []byte(`{"name":"Bob","age":32,"secret":"Shhh!"}`) var x person _ = json.Unmarshal(data, &x) spew.Dump(x) } Run

10 Unknown types

Structs are nice, but what happens when you don't know the data type prior to unmarshaling?

11 Cases of "unknown input"

Input may be string or Number

123 "123"

Input may be object, or array of objects

{...} [{...},{...}]

Input may be success, or an error

{"success":true,"results":[...]} {"success":false,"error":"..."} 12 Number literal, or string to number

13 Number literal, or string to number

Perhaps you have a sloppy API, that sometimes returns strings, and sometimes numbers. (True story at Lana)

Sometimes:

{"id": 123456}

Other times:

{"id": "123456"} 14 Number literal, or string to number

// IntOrString is an int that may be unmarshaled from either a JSON number // literal, or a JSON string. type IntOrString int

func (i *IntOrString) UnmarshalJSON( []byte) error { var v int err := json.Unmarshal(bytes.Trim(d, `"`), &v) *i = IntOrString(v) return err } 15 Number literal, or string to number

func main() { data := []byte(`[123,"321"]`) x := make([]IntOrString, 0) if err := json.Unmarshal(data, &x); err != nil { panic(err) } spew.Dump(x) } Run

16 Array or single element

17 Array or single element

Some sloppy APIs return either a single element, or an array of elements.

One result:

{"results": {"id": 1, ...} }

Multiple results:

{"results": [{"id": 1, ...}, {"id": 2, ...}] } 18 Array or single element

// SliceOrString is a slice of strings that may be unmarshaled from either // a JSON array of strings, or a single JSON string. type SliceOrString []string

func (s *SliceOrString) UnmarshalJSON(d []byte) error { if d[0] == '"' { var v string err := json.Unmarshal(d, &v) *s = SliceOrString{v} return err } var v []string err := json.Unmarshal(d, &v) *s = SliceOrString(v) return err } 19 Array or single element

func main() { data := []byte(`["one", ["two","elements"]]`) x := make([]SliceOrString, 0) if err := json.Unmarshal(data, &x); err != nil { panic(err) } spew.Dump(x) } Run

20 Unknown types

21 Unknown types

Many APIs will return either a successful response, or an error. Some use common fields (i.e. status) across both types of responses, some don't.

Success:

{"results": [ ... ]}

Failure:

{"error":"not found", "reason":"The requested object does not exist"}

There are a number of approaches to this problem. 22 Unknown types

First, create our distinct success and error types...

type Success struct { Results []string `json:"results"` }

type Error struct { Error string `json:"error"` Reason string `json:"reason"` } 23 Unknown types

In this simple example, because no fields are shared between the types, we can simply embed both types in a wrapper.

type Response struct { Success Error } 24 Unknown types

func main() { data := []byte(`[ {"results": ["one", "two"]}, {"error":"not found", "reason": "The requested object does not exist"} ]`) x := make([]Response, 0) if err := json.Unmarshal(data, &x); err != nil { panic(err) } spew.Dump(x) } Run

25 Unknown types, #2

26 Unknown types, #2

Let's imagine a less straight-forward situation:

Success:

{"status":"ok", "results": [ ... ]}

Failure:

{"status":"not found", "reason":"The requested object does not exist"} 27 Unknown types, #2

First, create our distinct success and error types...

type Success struct { Status string `json:"status"` Results []string `json:"results"` }

type Error struct { Status string `json:"status"` Reason string `json:"reason"` } 28 Unknown types, #2

Because the embedded fields conflict, we must be more explicit.

type Response struct { Success Error }

func (r *Response) UnmarshalJSON(d []byte) error { if err := json.Unmarshal(d, &r.Success); err != nil { return err } if r.Success.Status == "ok" { return nil } r.Success = Success{} return json.Unmarshal(d, &r.Error) } 29 Unknown types, #2

func main() { data := []byte(`[ {"status":"ok","results": ["one", "two"]}, {"status":"not found", "reason": "The requested object does not exist"} ]`) x := make([]Response, 0) if err := json.Unmarshal(data, &x); err != nil { panic(err) } spew.Dump(x) } Run

30 Using a container

31 Using a container

Bundling success and error objects like this can be cumbersome. We can use a different container type.

type Response struct { Result interface{} }

func (r *Response) UnmarshalJSON(d []byte) error { var success Success if err := json.Unmarshal(d, &success); err != nil { return err } if success.Status == "ok" { r.Result = success return nil } var fail Error if err := json.Unmarshal(d, &fail); err != nil { return err } r.Result = fail return nil } 32 Using a container

func main() { data := []byte(`[ {"status":"ok","results": ["one", "two"]}, {"status":"not found", "reason": "The requested object does not exist"} ]`) x := make([]Response, 0) if err := json.Unmarshal(data, &x); err != nil { panic(err) } spew.Dump(x) } Run

33 Using a container, #2

34 Using a container, #2

The container can also be a slice (or map) of objects, rather than a single object.

type Responses []interface{}

func (r *Responses) UnmarshalJSON(d []byte) error { var raw []json.RawMessage if err := json.Unmarshal(d, &raw); err != nil { return err } var success Success var fail Error result := make(Responses, len(raw)) for i, msg := range raw { _ = json.Unmarshal(msg, &success) if success.Status == "ok" { result[i] = success continue } _ = json.Unmarshal(msg, &fail) result[i] = fail } *r = result return nil } 35 Using a container, #2

func main() { data := []byte(`[ {"status":"ok","results": ["one", "two"]}, {"status":"not found", "reason": "The requested object does not exist"} ]`) var x Responses if err := json.Unmarshal(data, &x); err != nil { panic(err) } spew.Dump(x) } Run

36 Unknown types, #3

37 Unknown types, #3

Let's suppose we may receive any of three types:

{"type": "person", "name": "Bob", "age": 32}

{"type": "animal", "species": "dog", "name": "Spot"}

{"type": "address", "city": "Rotterdam", "street": "Goudsesingel"} 38 Unkwnown tyes, #3

type Person struct { Name string `json:"name"` Age int `json:"age"` }

type Animal struct { Species string `json:"species"` Name string `json:"name"` }

type Address struct { City string `json:"city"` Street string `json:"street"` }

type Responses []interface{} 39 Unknown types, #3

func (r *Responses) UnmarshalJSON(d []byte) error { var tmp []json.RawMessage if err := json.Unmarshal(d, &tmp); err != nil { return err } var header struct { Type string `json:"type"` } result := make(Responses, len(tmp)) continued... 40 Unknown types, #3

for i, raw := range tmp { _ = json.Unmarshal(raw, &header) switch header.Type { case "person": tgt := Person{} _ = json.Unmarshal(raw, &tgt) result[i] = tgt case "animal": tgt := Animal{} _ = json.Unmarshal(raw, &tgt) result[i] = tgt case "address": tgt := Address{} _ = json.Unmarshal(raw, &tgt) result[i] = tgt } } *r = result return nil } 41 Unknown types, #3

func main() { data := []byte(`[ {"type": "person", "name": "Bob", "age": 32}, {"type": "animal", "species": "dog", "name": "Spot"}, {"type": "address", "city": "Rotterdam", "street": "Goudsesingel"} ]`) var x Responses if err := json.Unmarshal(data, &x); err != nil { panic(err) } spew.Dump(x) } Run

42 Unknown types, #4

43 Unknown types, #4

The last example works well, but is not efficient. We can do better. 44 Unknown types, #4

func (r *Responses) UnmarshalJSON(d []byte) error { dec := json.NewDecoder(bytes.NewReader(d)) _, _ = dec.Token() // Consume opening "[" result := make(Responses, 0) var header struct { Type string `json:"type"` } continued... 45 Unknown types, #4

var raw json.RawMessage for dec.More() { _ = dec.Decode(&raw) _ = json.Unmarshal(raw, &header) switch header.Type { case "person": tgt := Person{} _ = json.Unmarshal(raw, &tgt) result = append(result, tgt) case "animal": tgt := Animal{} _ = json.Unmarshal(raw, &tgt) result = append(result, tgt) case "address": tgt := Address{} _ = json.Unmarshal(raw, &tgt) result = append(result, tgt) } } *r = result return nil } 46 Unknown types, #4

func main() { data := []byte(`[ {"type": "person", "name": "Bob", "age": 32}, {"type": "animal", "species": "dog", "name": "Spot"}, {"type": "address", "city": "Rotterdam", "street": "Goudsesingel"} ]`) var x Responses if err := json.Unmarshal(data, &x); err != nil { panic(err) } spew.Dump(x) } Run

47 Hybrid struct/map

48 Hybrid struct/map

Maybe your API sends a response with some known, and some unknown fields.

{"_id":"bob","type":"user","name":"Bob"}

{"_id":"meetup","type":"website","url":"https://meetup.com/"}

{"_id":"soup","type":"recipe","ingredients":["broth","chicken","noodles"]} 49 Hybrid struct/map

type Item struct { ID string `json:"_id"` Type string `json:"type"` Data map[string]interface{} `json:"-"` } 50 Hybrid struct/map

func (i *Item) UnmarshalJSON(d []byte) error { var x struct { Item UnmarshalJSON struct{} } if err := json.Unmarshal(d, &x); err != nil { return err }

var y map[string]interface{} _ = json.Unmarshal(d, &y) delete(y, "_id") delete(y, "type") *i = x.Item i.Data = y return nil } 51 Hybrid struct/map

func main() { data := []byte(`[ {"_id":"bob","type":"user","name":"Bob"}, {"_id":"meetup","type":"website","url":"https://meetup.com/"}, {"_id":"soup","type":"recipe","ingredients":["broth","chicken","noodles"]} ]`) x := []Item{} if err := json.Unmarshal(data, &x); err != nil { panic(err) } spew.Dump(x) } Run

52 Encoding a hybrid struct/map

53 Encoding a hybrid struct/map

func (i *Item) MarshalJSON() ([]byte, error) { data, _ := json.Marshal(i.Data) tmp := struct{ *Item MarshalJSON struct{} `json:"-"` }{Item: i} obj, _ := json.Marshal(tmp) obj[len(obj)-1] = ',' return append(obj, data[1:]...), nil } 54 Encoding a hybrid struct/map

func main() { x := []Item{ {ID: "bob", Type:"user",Data:map[string]interface{}{"name":"Bob"}}, {ID:"meetup",Type:"website",Data:map[string]interface{}{"url":"https://meetup.com/"}}, {ID:"soup",Type:"recipe",Data:map[string]interface{}{"ingredients":[]string{"broth","chicken","n } data, err := json.MarshalIndent(x, "", " ") if err != nil { panic(err) } fmt.Println(string(data)) } Run

55 Questions?

Notes, links, slides online:

jhall.io/posts/go-json-advanced (https://jhall.io/posts/go-json-advanced) 56 Thank you 19:40 05 Mar 2020 Jonathan Hall DevOps Evangelist / Go Developer / Clean Coder / Salsa Dancer [email protected] (mailto:[email protected]) https://jhall.io/ (https://jhall.io/)

@DevOpsHabits (http://twitter.com/DevOpsHabits)