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 {
		// ...
	}
}