JSON, interfaces, and go generate
Francesc Campoy
Developer, Advocate, and Gopher
Francesc Campoy
Developer, Advocate, and Gopher
Your mission, should you choose to accept it, is to decode this message:
{ "name": "Gopher", "birthdate": "2009/11/10", "shirt-size": "XS" }
into:
type Person struct { Name string Born time.Time Size ShirtSize }
Where ShirtSize is an enum (1):
type ShirtSize byte const ( NA ShirtSize = iota XS S M L XL )
(1): Go doesn't have enums.
In this talk I will refer to constants of integer types as enums.
Pros: very simple
Cons: too simple? we have to write extra code
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"]
Time format based on a "magic" date:
Mon Jan 2 15:04:05 -0700 MST 2006
An example:
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")) }
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:
Since our input was:
{ "name": "Gopher", "birthdate": "2009/11/10", "shirt-size": "XS" }
Parse the birth date:
born, err := time.Parse("2006/01/02", fields["birthdate"]) if err != nil { return fmt.Errorf("invalid date: %v", err) } p.Born = born
Many ways of writing this, this is a pretty bad one:
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 }
Use a switch statement, but a map is more compact.
Our complete parsing function:
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 }
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) }
Note: ShirtSize is a fmt.Stringer
Use tags to adapt field names:
type Person struct { Name string `json:"name"` Born time.Time `json:"birthdate"` Size ShirtSize `json:"shirt-size"` }
But this doesn't fit:
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) }
Use string fields and do any decoding manually afterwards.
var aux struct { Name string Born string `json:"birthdate"` Size string `json:"shirt-size"` }
Note: the field tag for Name is not needed; the JSON decoder performs a case
insensitive match if the exact form is not found.
The rest of the Parse function doesn't change much:
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 }
Repetition if other types have fields with:
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!
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
}Replace:
func (p *Person) Parse(s string) error {
with:
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)) if err := dec.Decode(&aux); err != nil { return fmt.Errorf("decode person: %v", err) } p.Name = aux.Name // ... rest of function omitted ...
And our main function becomes:
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) }
Substitute ParseShirtSize:
func ParseShirtSize(s string) (ShirtSize, error) {
with UnmarshalJSON:
func (ss *ShirtSize) UnmarshalJSON(data []byte) error { // Extract the string from data. var s string if err := json.Unmarshal(data, &s); err != nil { 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 return nil }
Now use ShirtSize in the aux struct:
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"` } 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 // ... 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)
}
Use the same trick to parse the birthdate.
Create a new type Date:
type Date struct{ time.Time }
And make it a json.Unmarshaler:
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 }
Now use Date in the aux struct:
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"` 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 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)
}
Can this code be shorter?
By making the Born field in Person of type Date.
Person.UnmarshalJSON is then equivalent to the default behavior!
It can be safely removed.
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) }
Because why not?
type romanNumeral int
And because Roman numerals are classier
type Movie struct { Title string Year romanNumeral }
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") 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) }
Some data is never to be encoded in clear text.
type Person struct { Name string `json:"name"` SSN secret `json:"ssn"` } type secret string
Use cryptography to make sure this is safe:
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)) }
Note: This solution is just a toy; don't use it for real systems.
And use the same key to decode it when it comes back:
func (s *secret) UnmarshalJSON(data []byte) error { var text string if err := json.Unmarshal(data, &text); err != nil { return fmt.Errorf("deocde secret string: %v", err) } cypher, err := base64.StdEncoding.DecodeString(text) if err != nil { return err } raw, err := rsa.DecryptOAEP(crypto.SHA512.New(), rand.Reader, key, cypher, nil) if err == nil { *s = secret(raw) } return err }
Let's try it:
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) }
go generate:
go buildYou 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 post.
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.
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/typesAnd generates the code using:
text/templateAnd it's on github: github.com/campoy/jsonenums