Handcrafting Enums Sucks

Generating Go enums that can be serialized to JSON strings.

Tagged with: go, programming

Published on and last updated on

Go is one of my go-to languages (pun intended) when I want to quickly build something or try something out. Recently I built a small HTTP API that returned an entity as JSON. This entity had a member that was an enum type. Go doesn’t have enums like some other languages but it allows the imitation of enums by using a type alias, constant groups, and the keyword iota.

type Pet int

const (
    cat Pet = iota
    dog
    pig
)

The values of the Pet type above will start at 0 for the cat and end with 2 for the pig. That’s the way how the iota keyword works. Now I defined the enum but when I serialize one of its values to JSON it is transformed into a number. An alternative would be to define the enum by using a string alias and assigning string values.

type PetString string

const (
    cat PetString = "cat"
    dog = "dog"
    pig = "pig"
)

By using the strings to define the values of the enum a conversion to JSON would return string values. But now I’ve lost the cool numbering features of iota and everything feels a bit repetitive.

Being the responsible software engineer I am I went on to find an alternative. Naturally the great go generate command came to my rescue. This command is bundled with the Go tools and can be used to execute commands that are specified inside of Go source files in special comments. Such a comment can look like this //go:generate touch foo. This would execute the touch command on a file called foo and therefore update the files timestamp or create the file if it didn’t exist. The funny thing is that you can actually run any Go source file by calling go run mycode.go in a Go generate comment. That means that you can write source code that generates other source code.

Now I knew what I wanted to do. I wanted to write a code generator that generates my enums and all the custom JSON conversion logic from and to strings. To generate my code I wrote a Go file that uses the jennifer package to generate Go code in a fluent fashion. After passing the values of my Pet enum to my generator the following code is generated. I removed the package, import, and JSON serialization code from the code for brevity.

type Pet int

const (
    cat Pet = iota
    dog
    pig
)

func (s Pet) String() string {
    return toStringPet[s]
}

var toStringPet = map[Pet]string{
    cat: "cat",
    dog: "dog",
    pig: "pig",
}
var toIdPet = map[string]Pet{
    "cat": cat,
    "dog": dog,
    "pig": pig,
}

func (s Pet) MarshalJSON() ([]byte, error) {
    // ... marshaling logic
}
func (s *Pet) UnmarshalJSON(b []byte) error {
    // ... unmarshaling logic
}

The complete code for my generator can be found on GitHub. After the enums are generated I can reference the enums in my code and serialize them to JSON as strings. Another benefit of using a code generator is that I could actually generate the enums by calling another API or reading in a file that provides the values. All in all, I am very happy with my solution and think that it may come in quite handy in some situations.