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)
+}