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:
// +build ignore,OMIT
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:
7Since 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 }
// +build ignore,OMIT
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:
// +build ignore,OMIT
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:
// +build ignore,OMIT
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 equivalent 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:
//go:build ignore && OMIT
// +build ignore,OMIT
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 equivalent 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.
24
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:
// +build ignore,OMIT
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?
26
By making the Born
field in Person
of type Date
.
Person.UnmarshalJSON
is then equivalent to the default behavior!
It can be safely removed.
// +build ignore,OMIT
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 }
// +build ignore,OMIT
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.
34And 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:
// +build ignore,OMIT
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 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 post.
38
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/types
And generates the code using:
text/template
And it's on github: github.com/campoy/jsonenums
40