blob: 19918195d0e5f24c5c9a72e786cf04e5b32532e8 [file] [log] [blame]
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 [[http://blog.golang.org/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: [[http://github.com/campoy/jsonenums][github.com/campoy/jsonenums]]
* Demo