| JSON, interfaces, and go generate | |
| Francesc Campoy | |
| Developer, Advocate, and Gopher | |
| @francesc | |
| campoy@golang.org | |
| * Your mission | |
| Your mission, should you choose to accept it, is to decode this message: | |
| .code json/unmarshaler0map.go /^{/,/^}/ | |
| into: | |
| .code json/unmarshaler0.go /type Person/,/^}/ | |
| * Your mission (cont.) | |
| Where `ShirtSize` is an enum _(1)_: | |
| .code json/unmarshaler0.go /type ShirtSize byte/,/^\)/ | |
| _(1)_: Go doesn't have enums. | |
| In this talk I will refer to constants of integer types as enums. | |
| * Using a map | |
| * Using a map | |
| _Pros_: very simple | |
| _Cons_: too simple? we have to write extra code | |
| .code json/unmarshaler0map.go /Person.*Parse/,/p.Name/ | |
| * Parsing dates | |
| Time format based on a "magic" date: | |
| Mon Jan 2 15:04:05 -0700 MST 2006 | |
| An example: | |
| .play json/dates.go /func main/, | |
| * Why that date? | |
| Let's reorder: | |
| Mon Jan 2 15:04:05 -0700 MST 2006 | |
| into: | |
| 01/02 03:04:05 PM 2006 -07:00 MST | |
| which is: | |
| * 1 2 3 4 5 6 7! | |
| .image json/img/mindblown.gif 500 _ | |
| * Parsing the birth date: | |
| Since our input was: | |
| .code json/unmarshaler0map.go /^{/,/^}/ | |
| Parse the birth date: | |
| .code json/unmarshaler0map.go /time.Parse/,/p.Born/ | |
| * Parsing the shirt size | |
| Many ways of writing this, this is a pretty bad one: | |
| .code json/unmarshaler0map.go /ParseShirtSize/,/^}/ | |
| Use a `switch` statement, but a map is more compact. | |
| * Parsing the shirt size | |
| Our complete parsing function: | |
| .code json/unmarshaler0map.go /Person.*Parse/,/^}/ | |
| * Does this work? | |
| .play json/unmarshaler0map.go /func main/,/^}/ | |
| _Note_: `ShirtSize` is a `fmt.Stringer` | |
| * JSON decoding into structs | |
| * JSON decoding into structs | |
| Use tags to adapt field names: | |
| .code json/unmarshaler0bad.go /type Person/,/^}/ | |
| But this doesn't fit: | |
| .play json/unmarshaler0bad.go /func main/, | |
| * Let's use an auxiliary struct type | |
| Use string fields and do any decoding manually afterwards. | |
| .code json/unmarshaler0.go /var aux struct/,/}/ | |
| _Note_: the field tag for `Name` is not needed; the JSON decoder performs a case | |
| insensitive match if the exact form is not found. | |
| * Let's use an auxiliary struct type (cont.) | |
| The rest of the `Parse` function doesn't change much: | |
| .code json/unmarshaler0.go /Person.*Parse/,/^}/ | |
| * Can we do better? | |
| * Current solution | |
| Repetition if other types have fields with: | |
| - date fields with same formatting, | |
| - or t-shirt sizes. | |
| Let's make the types smarter so `json.Decoder` will do all the work transparently. | |
| *Goal*: `json.Decoder` should do all the work for me! | |
| * Meet Marshaler and Unmarshaler | |
| Types satisfying `json.Marshaler` define how to be encoded into json. | |
| type Marshaler interface { | |
| MarshalJSON() ([]byte, error) | |
| } | |
| And `json.Unmarshaler` for the decoding part. | |
| type Unmarshaler interface { | |
| UnmarshalJSON([]byte) error | |
| } | |
| * UnmarshalJSON all the things! | |
| * Let's make Person a json.Unmarshaler | |
| Replace: | |
| .code json/unmarshaler0.go /Person.*Parse/ | |
| with: | |
| .code json/unmarshaler1.go /Person.*UnmarshalJSON/,/rest of function/ | |
| * Let's make Person a json.Unmarshaler (cont.) | |
| And our `main` function becomes: | |
| .play json/unmarshaler1.go /func main/,/^}/ | |
| * UnmarshalJSON for enums | |
| Substitute `ParseShirtSize`: | |
| .code json/unmarshaler1.go /ParseShirtSize/ | |
| with `UnmarshalJSON`: | |
| .code json/unmarshaler2.go /ShirtSize.*UnmarshalJSON/,/^}/ | |
| * UnmarshalJSON for enums (cont.) | |
| Now use `ShirtSize` in the aux struct: | |
| .play json/unmarshaler2.go /Person.*UnmarshalJSON/,/rest of function/ | |
| Use the same trick to parse the birthdate. | |
| * Unmarshaling differently formatted dates | |
| Create a new type `Date`: | |
| .code json/unmarshaler3.go /type Date/ | |
| And make it a `json.Unmarshaler`: | |
| .code json/unmarshaler3.go /Date.*UnmarshalJSON/,/^}/ | |
| * Unmarshaling differently formatted dates (cont.) | |
| Now use `Date` in the aux struct: | |
| .play json/unmarshaler3.go /Person.*UnmarshalJSON/,/^}/ | |
| Can this code be shorter? | |
| * Yes! | |
| By making the `Born` field in `Person` of type `Date`. | |
| `Person.UnmarshalJSON` is then equivalent to the default behavior! | |
| It can be safely removed. | |
| .play json/unmarshaler4.go /func main/,/^}/ | |
| * Was this really better? | |
| - Code length: 86LoC vs 80LoC | |
| - Reusability of types | |
| - Easier to maintain | |
| - Usage of the standard library | |
| * Other ideas | |
| * Roman numerals | |
| * Roman numerals | |
| Because why not? | |
| .code json/roman_numerals.go /type romanNumeral/ | |
| And because Roman numerals are classier | |
| .code json/roman_numerals.go /type Movie/,/^}/ | |
| * Roman numerals (cont.) | |
| .play json/roman_numerals.go /func main/,/^}/ | |
| * Secret data | |
| * Secret data | |
| Some data is never to be encoded in clear text. | |
| .code json/secret.go /type Person/,/type secret/ | |
| Use cryptography to make sure this is safe: | |
| .code json/secret.go /secret.*MarshalJSON/,/^}/ | |
| _Note_: This solution is just a toy; don't use it for real systems. | |
| * Secret data (cont.) | |
| And use the same key to decode it when it comes back: | |
| .code json/secret.go /secret.*UnmarshalJSON/,/^}/ | |
| * Secret data (cont.) | |
| Let's try it: | |
| .play json/secret.go /func main/,/^}/ | |
| * But most JSON enums are boring | |
| * go generate to the rescue! | |
| `go`generate`: | |
| - introduced in Go 1.4 | |
| - a tool for package authors | |
| - an extra step before `go`build` | |
| You will see it as comments in the code like: | |
| //go:generate go tool yacc -o gopher.go -p parser gopher.y | |
| More information in the [[/blog/generate][blog post]]. | |
| * code generation tools: stringer | |
| `stringer` generates `String` methods for enum types. | |
| package painkiller | |
| //go:generate stringer -type=Pill | |
| type Pill int | |
| const ( | |
| Placebo Pill = iota | |
| Aspirin | |
| Ibuprofen | |
| Paracetamol | |
| ) | |
| Call `go`generate`: | |
| $ go generate $GOPATH/src/path_to_painkiller | |
| which will create a new file containing the `String` definition for `Pill`. | |
| * jsonenums | |
| Around 200 lines of code. | |
| Parses and analyses a package using: | |
| - `go/{ast/build/format/parser/token}` | |
| - `golang.org/x/tools/go/exact`, `golang.org/x/tools/go/types` | |
| And generates the code using: | |
| - `text/template` | |
| And it's on github: [[https://github.com/campoy/jsonenums][github.com/campoy/jsonenums]] | |
| * Demo |