Go JSON unmarshaling based on an enumerated field value
In a previous article , we talked about marshaling/unmarshaling JSON with a structure like
{
"type": "this part tells you how to interpret the message",
"msg": ...the actual message is here, in some kind of json...
}
Last time, we left a repetitive switch statement in the code, where each message type was unmarshaled very explicitly. This time, we'll talk about ways to clean that up.
Previous solution
This is what we ended up with, in the previous article :
package main
import (
"encoding/json"
"fmt"
"log"
)
const input = `
{
"type": "sound",
"msg": {
"description": "dynamite",
"authority": "the Bruce Dickinson"
}
}
`
type Envelope struct {
Type string
Msg interface{}
}
type Sound struct {
Description string
Authority string
}
func main() {
var msg json.RawMessage
env := Envelope{
Msg: &msg,
}
if err := json.Unmarshal([]byte(input), &env); err != nil {
log.Fatal(err)
}
switch env.Type {
case "sound":
var s Sound
if err := json.Unmarshal(msg, &s); err != nil {
log.Fatal(err)
}
var desc string = s.Description
fmt.Println(desc)
// more cases here
default:
log.Fatalf("unknown message type: %q", env.Type)
}
}
Output:
dynamite
Now, let's rewrite that without an explicit json.Unmarshal
call in
every switch branch.
Detour: jsonenums
github.com/campoy/jsonenums
generates code for parsing JSON strings into Go constants, as
enumerated values. We'll use it to create a type for our Type
field.
We'll call it Kind
to minimize confusion with the type
keyword.
//go:generate jsonenums -type=Kind
type Kind int
const (
sound Kind = iota
cowbell
)
The generated code will include a UnmarshalJSON
method for Kind
that understands the JSON input "sound"
as the constant value
sound
(whatever int
value that ends up being; here 1).
To avoid confusion between sound
and Sound
, let's rename our
earlier type Sound
to SoundMsg
.
Unmarshaling with Kind
We can now update our earlier unmarshaling logic to use the constants,
which also allows us to skip the explicit validation; jsonenums
will
handle that.
type Envelope struct {
Type Kind
Msg interface{}
}
type SoundMsg struct {
Description string
Authority string
}
func main() {
var msg json.RawMessage
env := Envelope{
Msg: &msg,
}
if err := json.Unmarshal([]byte(input), &env); err != nil {
log.Fatal(err)
}
switch env.Type {
case sound:
var s SoundMsg
if err := json.Unmarshal(msg, &s); err != nil {
log.Fatal(err)
}
var desc string = s.Description
fmt.Println(desc)
// more cases here
}
}
Avoiding the manual unmarshal
With the above, all of the switch cases still begin with very
repetitive code, so next we'll abstract out the common part: based on
Kind
, they unmarshal a json.RawMessage
to whatever is the correct
Go type.
All we need is a map from Kind
to something that makes a new value
that can be unmarshaled to:
var kindHandlers = map[Kind]func() interface{}{
sound: func() interface{} { return &SoundMsg{} },
cowbell: func() interface{} { return &CowbellMsg{} },
}
You could code generate this map (or a similar switch), if you wanted to.
To cleanly separate the json.RawMessage
from the unmarshaled message
(whatever type it is), let's rename the former to raw
, and call the
latter msg
.
func main() {
var raw json.RawMessage
env := Envelope{
Msg: &raw,
}
if err := json.Unmarshal([]byte(input), &env); err != nil {
log.Fatal(err)
}
msg := kindHandlers[env.Type]()
if err := json.Unmarshal(raw, msg); err != nil {
log.Fatal(err)
}
}
Now, if we want to do the same demonstration print as the last time, we'll put back the switch.
switch msg := msg.(type) {
case *SoundMsg:
fmt.Println(msg.Description)
We could also just pass the message as interface{}
to any function.
We can also use a more descriptive instead of interface{}
, if all
our XxxMsg
types implement some useful interface. Let's do that
next.
Full demonstration program
package main
import (
"encoding/json"
"fmt"
"log"
)
const input = `
{
"type": "sound",
"msg": {
"description": "dynamite",
"authority": "the Bruce Dickinson"
}
}
`
//go:generate jsonenums -type=Kind
type Kind int
const (
sound Kind = iota
cowbell
)
type Envelope struct {
Type Kind
Msg interface{}
}
type SoundMsg struct {
Description string
Authority string
}
type CowbellMsg struct {
More bool
}
var kindHandlers = map[Kind]func() interface{}{
sound: func() interface{} { return &SoundMsg{} },
cowbell: func() interface{} { return &CowbellMsg{} },
}
func main() {
var raw json.RawMessage
env := Envelope{
Msg: &raw,
}
if err := json.Unmarshal([]byte(input), &env); err != nil {
log.Fatal(err)
}
msg := kindHandlers[env.Type]()
if err := json.Unmarshal(raw, msg); err != nil {
log.Fatal(err)
}
switch msg := msg.(type) {
case *SoundMsg:
fmt.Println(msg.Description)
}
}
Output:
dynamite
Methods instead of type switches
The above example still has a type switch, where it actually wants to access the message fields. Here's how you could move that functionality into methods on the messages:
type App struct {
// whatever your application state is
}
// Action is something that can operate on the application.
type Action interface {
Run(app *App) error
}
type CowbellMsg struct {
// ...
}
func (m *CowbellMsg) Run(app *App) error {
// ...
}
type SoundMsg struct {
// ...
}
func (m *SoundMsg) Run(app *App) error {
// ...
}
var kindHandlers = map[Kind]func() Action{
sound: func() Action { return &SoundMsg{} },
cowbell: func() Action { return &CowbellMsg{} },
}
func main() {
app := &App{
// ...
}
// process an incoming message
var raw json.RawMessage
env := Envelope{
Msg: &raw,
}
if err := json.Unmarshal([]byte(input), &env); err != nil {
log.Fatal(err)
}
msg := kindHandlers[env.Type]()
if err := json.Unmarshal(raw, msg); err != nil {
log.Fatal(err)
}
if err := msg.Run(app); err != nil {
// ...
}
}