talks: adding JSON talk
Change-Id: Id6a53c8e631f36348fb93e4adaf99d547de605ef
Reviewed-on: https://go-review.googlesource.com/3506
Reviewed-by: Andrew Gerrand <adg@golang.org>
diff --git a/2015/json.slide b/2015/json.slide
new file mode 100644
index 0000000..1991819
--- /dev/null
+++ b/2015/json.slide
@@ -0,0 +1,319 @@
+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
diff --git a/2015/json/dates.go b/2015/json/dates.go
new file mode 100644
index 0000000..2502f4c
--- /dev/null
+++ b/2015/json/dates.go
@@ -0,0 +1,14 @@
+package main
+
+import (
+ "fmt"
+ "time"
+)
+
+func main() {
+ now := time.Now()
+ fmt.Printf("Standard format: %v\n", now)
+ fmt.Printf("American format: %v\n", now.Format("Jan 2 2006"))
+ fmt.Printf("European format: %v\n", now.Format("02/01/2006"))
+ fmt.Printf("Chinese format: %v\n", now.Format("2006/01/02"))
+}
diff --git a/2015/json/img/mindblown.gif b/2015/json/img/mindblown.gif
new file mode 100644
index 0000000..7cf3738
--- /dev/null
+++ b/2015/json/img/mindblown.gif
Binary files differ
diff --git a/2015/json/roman_numerals.go b/2015/json/roman_numerals.go
new file mode 100644
index 0000000..c802100
--- /dev/null
+++ b/2015/json/roman_numerals.go
@@ -0,0 +1,86 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "strings"
+)
+
+type romanNumeral int
+
+var numerals = []struct {
+ s string
+ v int
+}{
+ {"M", 1000}, {"CM", 900},
+ {"D", 500}, {"CD", 400},
+ {"C", 100}, {"XC", 90},
+ {"L", 50}, {"XL", 40},
+ {"X", 10}, {"IX", 9},
+ {"V", 5}, {"IV", 4},
+ {"I", 1},
+}
+
+func (n romanNumeral) String() string {
+ res := ""
+ v := int(n)
+ for _, num := range numerals {
+ res += strings.Repeat(num.s, v/num.v)
+ v %= num.v
+ }
+ return res
+}
+
+func parseRomanNumeral(s string) (romanNumeral, error) {
+ res := 0
+ for _, num := range numerals {
+ for strings.HasPrefix(s, num.s) {
+ res += num.v
+ s = s[len(num.s):]
+ }
+ }
+ return romanNumeral(res), nil
+}
+
+func (n romanNumeral) MarshalJSON() ([]byte, error) {
+ if n <= 0 {
+ return nil, fmt.Errorf("Romans had only natural (=>1) numbers")
+ }
+ return json.Marshal(n.String())
+}
+
+func (n *romanNumeral) UnmarshalJSON(data []byte) error {
+ var s string
+ if err := json.Unmarshal(data, &s); err != nil {
+ return err
+ }
+ p, err := parseRomanNumeral(s)
+ if err == nil {
+ *n = p
+ }
+ return err
+}
+
+type Movie struct {
+ Title string
+ Year romanNumeral
+}
+
+func main() {
+ // Encoding
+ movies := []Movie{{"E.T.", 1982}, {"The Matrix", 1999}, {"Casablanca", 1942}}
+ res, err := json.MarshalIndent(movies, "", "\t") // HL
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("Movies: %s\n", res)
+
+ // Decoding
+ var m Movie
+ inputText := `{"Title": "Alien", "Year":"MCMLXXIX"}`
+ if err := json.NewDecoder(strings.NewReader(inputText)).Decode(&m); err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("%s was released in %d\n", m.Title, m.Year)
+}
diff --git a/2015/json/secret.go b/2015/json/secret.go
new file mode 100644
index 0000000..698bf2d
--- /dev/null
+++ b/2015/json/secret.go
@@ -0,0 +1,72 @@
+package main
+
+import (
+ "crypto"
+ "crypto/rand"
+ "crypto/rsa"
+ _ "crypto/sha512"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "log"
+)
+
+var key *rsa.PrivateKey
+
+func init() {
+ k, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ log.Fatalf("generate key: %v", err)
+ }
+ key = k
+}
+
+type Person struct {
+ Name string `json:"name"`
+ SSN secret `json:"ssn"`
+}
+
+type secret string
+
+func (s secret) MarshalJSON() ([]byte, error) {
+ m, err := rsa.EncryptOAEP(crypto.SHA512.New(), rand.Reader, key.Public().(*rsa.PublicKey), []byte(s), nil)
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(base64.StdEncoding.EncodeToString(m))
+}
+
+func (s *secret) UnmarshalJSON(data []byte) error {
+ var text string
+ if err := json.Unmarshal(data, &text); err != nil { // HL
+ return fmt.Errorf("deocde secret string: %v", err)
+ }
+ cypher, err := base64.StdEncoding.DecodeString(text) // HL
+ if err != nil {
+ return err
+ }
+ raw, err := rsa.DecryptOAEP(crypto.SHA512.New(), rand.Reader, key, cypher, nil) // HL
+ if err == nil {
+ *s = secret(raw)
+ }
+ return err
+}
+
+func main() {
+ p := Person{
+ Name: "Francesc",
+ SSN: "123456789",
+ }
+
+ b, err := json.MarshalIndent(p, "", "\t")
+ if err != nil {
+ log.Fatalf("Encode person: %v", err)
+ }
+ fmt.Printf("%s\n", b)
+
+ var d Person
+ if err := json.Unmarshal(b, &d); err != nil {
+ log.Fatalf("Decode person: %v", err)
+ }
+ fmt.Println(d)
+}
diff --git a/2015/json/unmarshaler0.go b/2015/json/unmarshaler0.go
new file mode 100644
index 0000000..1efbe96
--- /dev/null
+++ b/2015/json/unmarshaler0.go
@@ -0,0 +1,87 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "strings"
+ "time"
+)
+
+const input = `
+{
+ "name":"Gopher",
+ "birthdate": "2009/11/10",
+ "shirt-size": "XS"
+}
+`
+
+type Person struct {
+ Name string
+ Born time.Time
+ Size ShirtSize
+}
+
+func (p Person) String() string {
+ return fmt.Sprintf("%s was born on %v and uses a %v t-shirt",
+ p.Name, p.Born.Format("Jan 2 2006"), p.Size)
+}
+
+type ShirtSize byte
+
+const (
+ NA ShirtSize = iota
+ XS
+ S
+ M
+ L
+ XL
+)
+
+func (ss ShirtSize) String() string {
+ sizes := map[ShirtSize]string{XS: "XS", S: "S", M: "M", L: "L", XL: "XL"}
+ s, ok := sizes[ss]
+ if !ok {
+ return "invalid t-shirt size"
+ }
+ return s
+}
+
+func ParseShirtSize(s string) (ShirtSize, error) {
+ sizes := map[string]ShirtSize{"XS": XS, "S": S, "M": M, "L": L, "XL": XL}
+ ss, ok := sizes[s]
+ if !ok {
+ return NA, fmt.Errorf("invalid t-shirt size %q", s)
+ }
+ return ss, nil
+}
+
+func (p *Person) Parse(s string) error {
+ var aux struct {
+ Name string
+ Born string `json:"birthdate"`
+ Size string `json:"shirt-size"`
+ }
+
+ dec := json.NewDecoder(strings.NewReader(s))
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("decode person: %v", err)
+ }
+
+ p.Name = aux.Name
+ born, err := time.Parse("2006/01/02", aux.Born)
+ if err != nil {
+ return fmt.Errorf("invalid date: %v", err)
+ }
+ p.Born = born
+ p.Size, err = ParseShirtSize(aux.Size)
+ return err
+}
+
+func main() {
+ var p Person
+ if err := p.Parse(input); err != nil {
+ log.Fatalf("parse person: %v", err)
+ }
+ fmt.Println(p)
+}
diff --git a/2015/json/unmarshaler0bad.go b/2015/json/unmarshaler0bad.go
new file mode 100644
index 0000000..74bfdb0
--- /dev/null
+++ b/2015/json/unmarshaler0bad.go
@@ -0,0 +1,51 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "strings"
+ "time"
+)
+
+const input = `
+ {
+ "name":"Gopher",
+ "birthdate": "2009/11/10",
+ "shirt-size": "XS"
+ }
+ `
+
+type Person struct {
+ Name string `json:"name"`
+ Born time.Time `json:"birthdate"`
+ Size ShirtSize `json:"shirt-size"`
+}
+
+type ShirtSize byte
+
+const (
+ NA ShirtSize = iota
+ XS
+ S
+ M
+ L
+ XL
+)
+
+func (ss ShirtSize) String() string {
+ s, ok := map[ShirtSize]string{XS: "XS", S: "S", M: "M", L: "L", XL: "XL"}[ss]
+ if !ok {
+ return "invalid ShirtSize"
+ }
+ return s
+}
+
+func main() {
+ var p Person
+ dec := json.NewDecoder(strings.NewReader(input))
+ if err := dec.Decode(&p); err != nil {
+ log.Fatalf("parse person: %v", err)
+ }
+ fmt.Println(p)
+}
diff --git a/2015/json/unmarshaler0map.go b/2015/json/unmarshaler0map.go
new file mode 100644
index 0000000..22e142a
--- /dev/null
+++ b/2015/json/unmarshaler0map.go
@@ -0,0 +1,81 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "strings"
+ "time"
+)
+
+const input = `
+{
+ "name": "Gopher",
+ "birthdate": "2009/11/10",
+ "shirt-size": "XS"
+}
+`
+
+type Person struct {
+ Name string
+ Born time.Time
+ Size ShirtSize
+}
+
+type ShirtSize byte
+
+const (
+ NA ShirtSize = iota
+ XS
+ S
+ M
+ L
+ XL
+)
+
+func (ss ShirtSize) String() string {
+ sizes := map[ShirtSize]string{XS: "XS", S: "S", M: "M", L: "L", XL: "XL"}
+ s, ok := sizes[ss]
+ if !ok {
+ return "invalid ShirtSize"
+ }
+ return s
+}
+
+func ParseShirtSize(s string) (ShirtSize, error) {
+ sizes := map[string]ShirtSize{"XS": XS, "S": S, "M": M, "L": L, "XL": XL}
+ ss, ok := sizes[s]
+ if !ok {
+ return NA, fmt.Errorf("invalid ShirtSize %q", s)
+ }
+ return ss, nil
+}
+
+func (p *Person) Parse(s string) error {
+ fields := map[string]string{}
+
+ dec := json.NewDecoder(strings.NewReader(s))
+ if err := dec.Decode(&fields); err != nil {
+ return fmt.Errorf("decode person: %v", err)
+ }
+
+ // Once decoded we can access the fields by name.
+ p.Name = fields["name"]
+
+ born, err := time.Parse("2006/01/02", fields["birthdate"])
+ if err != nil {
+ return fmt.Errorf("invalid date: %v", err)
+ }
+ p.Born = born
+
+ p.Size, err = ParseShirtSize(fields["shirt-size"])
+ return err
+}
+
+func main() {
+ var p Person
+ if err := p.Parse(input); err != nil {
+ log.Fatalf("parse person: %v", err)
+ }
+ fmt.Println(p)
+}
diff --git a/2015/json/unmarshaler1.go b/2015/json/unmarshaler1.go
new file mode 100644
index 0000000..7b34681
--- /dev/null
+++ b/2015/json/unmarshaler1.go
@@ -0,0 +1,82 @@
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "log"
+ "strings"
+ "time"
+)
+
+const input = `
+{
+ "name": "Gopher",
+ "birthdate": "2009/11/10",
+ "shirt-size": "XS"
+}
+`
+
+type Person struct {
+ Name string
+ Born time.Time
+ Size ShirtSize
+}
+
+type ShirtSize byte
+
+const (
+ NA ShirtSize = iota
+ XS
+ S
+ M
+ L
+ XL
+)
+
+func (ss ShirtSize) String() string {
+ s, ok := map[ShirtSize]string{XS: "XS", S: "S", M: "M", L: "L", XL: "XL"}[ss]
+ if !ok {
+ return "invalid ShirtSize"
+ }
+ return s
+}
+
+func ParseShirtSize(s string) (ShirtSize, error) {
+ ss, ok := map[string]ShirtSize{"XS": XS, "S": S, "M": M, "L": L, "XL": XL}[s]
+ if !ok {
+ return NA, fmt.Errorf("invalid ShirtSize %q", s)
+ }
+ return ss, nil
+}
+
+func (p *Person) UnmarshalJSON(data []byte) error {
+ var aux struct {
+ Name string
+ Born string `json:"birthdate"`
+ Size string `json:"shirt-size"`
+ }
+
+ dec := json.NewDecoder(bytes.NewReader(data)) // HL
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("decode person: %v", err)
+ }
+ p.Name = aux.Name
+ // ... rest of function omitted ...
+ born, err := time.Parse("2006/01/02", aux.Born)
+ if err != nil {
+ return fmt.Errorf("invalid date: %v", err)
+ }
+ p.Born = born
+ p.Size, err = ParseShirtSize(aux.Size)
+ return err
+}
+
+func main() {
+ var p Person
+ dec := json.NewDecoder(strings.NewReader(input))
+ if err := dec.Decode(&p); err != nil {
+ log.Fatalf("parse person: %v", err)
+ }
+ fmt.Println(p)
+}
diff --git a/2015/json/unmarshaler2.go b/2015/json/unmarshaler2.go
new file mode 100644
index 0000000..141f5ba
--- /dev/null
+++ b/2015/json/unmarshaler2.go
@@ -0,0 +1,88 @@
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "log"
+ "strings"
+ "time"
+)
+
+const input = `{
+ "name":"Gopher",
+ "birthdate": "2009/11/10",
+ "shirt-size": "XS"
+}`
+
+type Person struct {
+ Name string
+ Born time.Time
+ Size ShirtSize
+}
+
+type ShirtSize byte
+
+const (
+ NA ShirtSize = iota
+ XS
+ S
+ M
+ L
+ XL
+)
+
+func (ss ShirtSize) String() string {
+ s, ok := map[ShirtSize]string{XS: "XS", S: "S", M: "M", L: "L", XL: "XL"}[ss]
+ if !ok {
+ return "invalid ShirtSize"
+ }
+ return s
+}
+
+func (ss *ShirtSize) UnmarshalJSON(data []byte) error {
+ // Extract the string from data.
+ var s string
+ if err := json.Unmarshal(data, &s); err != nil { // HL
+ return fmt.Errorf("shirt-size should be a string, got %s", data)
+ }
+
+ // The rest is equivalen to ParseShirtSize.
+ got, ok := map[string]ShirtSize{"XS": XS, "S": S, "M": M, "L": L, "XL": XL}[s]
+ if !ok {
+ return fmt.Errorf("invalid ShirtSize %q", s)
+ }
+ *ss = got // HL
+ return nil
+}
+
+func (p *Person) UnmarshalJSON(data []byte) error {
+ var aux struct {
+ Name string
+ Born string `json:"birthdate"`
+ Size ShirtSize `json:"shirt-size"` // HL
+ }
+
+ dec := json.NewDecoder(bytes.NewReader(data))
+ if err := dec.Decode(&aux); err != nil {
+ return fmt.Errorf("decode person: %v", err)
+ }
+ p.Name = aux.Name
+ p.Size = aux.Size // HL
+ // ... rest of function omitted ...
+ born, err := time.Parse("2006/01/02", aux.Born)
+ if err != nil {
+ return fmt.Errorf("invalid date: %v", err)
+ }
+ p.Born = born
+ return nil
+}
+
+func main() {
+ var p Person
+ dec := json.NewDecoder(strings.NewReader(input))
+ if err := dec.Decode(&p); err != nil {
+ log.Fatalf("parse person: %v", err)
+ }
+ fmt.Println(p)
+}
diff --git a/2015/json/unmarshaler3.go b/2015/json/unmarshaler3.go
new file mode 100644
index 0000000..49e4faa
--- /dev/null
+++ b/2015/json/unmarshaler3.go
@@ -0,0 +1,96 @@
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "log"
+ "strings"
+ "time"
+)
+
+const input = `{
+ "name":"Gopher",
+ "birthdate": "2009/11/10",
+ "shirt-size": "XS"
+}`
+
+type Person struct {
+ Name string
+ Born Date
+ Size ShirtSize
+}
+
+type ShirtSize byte
+
+const (
+ NA ShirtSize = iota
+ XS
+ S
+ M
+ L
+ XL
+)
+
+func (ss ShirtSize) String() string {
+ s, ok := map[ShirtSize]string{XS: "XS", S: "S", M: "M", L: "L", XL: "XL"}[ss]
+ if !ok {
+ return "invalid ShirtSize"
+ }
+ return s
+}
+
+func (ss *ShirtSize) UnmarshalJSON(data []byte) error {
+ var s string
+ if err := json.Unmarshal(data, &s); err != nil {
+ return fmt.Errorf("shirt-size should be a string, got %s", data)
+ }
+ got, ok := map[string]ShirtSize{"XS": XS, "S": S, "M": M, "L": L, "XL": XL}[s]
+ if !ok {
+ return fmt.Errorf("invalid ShirtSize %q", s)
+ }
+ *ss = got
+ return nil
+}
+
+type Date struct{ time.Time }
+
+func (d Date) String() string { return d.Format("2006/01/02") }
+
+func (d *Date) UnmarshalJSON(data []byte) error {
+ var s string
+ if err := json.Unmarshal(data, &s); err != nil {
+ return fmt.Errorf("birthdate should be a string, got %s", data)
+ }
+ t, err := time.Parse("2006/01/02", s) // HL
+ if err != nil {
+ return fmt.Errorf("invalid date: %v", err)
+ }
+ d.Time = t
+ return nil
+}
+
+func (p *Person) UnmarshalJSON(data []byte) error {
+ r := bytes.NewReader(data)
+ var aux struct {
+ Name string
+ Born Date `json:"birthdate"` // HL
+ Size ShirtSize `json:"shirt-size"`
+ }
+ if err := json.NewDecoder(r).Decode(&aux); err != nil {
+ return fmt.Errorf("decode person: %v", err)
+ }
+ p.Name = aux.Name
+ p.Size = aux.Size
+ p.Born = aux.Born // HL
+ return nil
+}
+
+func main() {
+ var p Person
+ dec := json.NewDecoder(strings.NewReader(input))
+ if err := dec.Decode(&p); err != nil {
+ log.Fatalf("parse person: %v", err)
+ }
+ fmt.Println(p)
+}
diff --git a/2015/json/unmarshaler4.go b/2015/json/unmarshaler4.go
new file mode 100644
index 0000000..1bad743
--- /dev/null
+++ b/2015/json/unmarshaler4.go
@@ -0,0 +1,79 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "strings"
+ "time"
+)
+
+const input = `{
+ "name":"Gopher",
+ "birthdate": "2009/11/10",
+ "shirt-size": "XS"
+}`
+
+type Person struct {
+ Name string `json:"name"`
+ Born Date `json:"birthdate"`
+ Size ShirtSize `json:"shirt-size"`
+}
+
+type ShirtSize byte
+
+const (
+ NA ShirtSize = iota
+ XS
+ S
+ M
+ L
+ XL
+)
+
+func (ss ShirtSize) String() string {
+ s, ok := map[ShirtSize]string{XS: "XS", S: "S", M: "M", L: "L", XL: "XL"}[ss]
+ if !ok {
+ return "invalid ShirtSize"
+ }
+ return s
+}
+
+func (ss *ShirtSize) UnmarshalJSON(data []byte) error {
+ var s string
+ if err := json.Unmarshal(data, &s); err != nil {
+ return fmt.Errorf("shirt-size should be a string, got %s", data)
+ }
+ got, ok := map[string]ShirtSize{"XS": XS, "S": S, "M": M, "L": L, "XL": XL}[s]
+ if !ok {
+ return fmt.Errorf("invalid ShirtSize %q", s)
+ }
+ *ss = got
+ return nil
+}
+
+type Date struct{ time.Time }
+
+func (d Date) String() string { return d.Format("2006/01/02") }
+
+func (d *Date) UnmarshalJSON(data []byte) error {
+ var s string
+ if err := json.Unmarshal(data, &s); err != nil {
+ return fmt.Errorf("birthdate should be a string, got %s", data)
+ }
+ t, err := time.Parse("2006/01/02", s)
+ if err != nil {
+ return fmt.Errorf("invalid date: %v", err)
+ }
+ d.Time = t
+ return nil
+}
+
+func main() {
+ var p Person
+ dec := json.NewDecoder(strings.NewReader(input))
+ if err := dec.Decode(&p); err != nil {
+ log.Fatalf("parse person: %v", err)
+ }
+ fmt.Println(p)
+}