Open-source project
dmarkham/enumer avatar
dmarkham/enumer

enumer: writing the String method your Go enum never had

A Go tool to auto generate methods for your enums

523 stars79 forksGoNOASSERTION

At a glance

What is it?
enumer is a Go code generator that turns a typed constant block into a String method plus parsers, encoders and validators. It runs from go generate, pins as a tool dependency, and is presented as a drop-in replacement for stringer.
Who is it for?
enumer is a good fit for a Go service that defines its state as typed constants and then needs those constants to survive JSON, YAML, SQL and gqlgen without writing four hand rolled marshalling files. It is the wrong tool for a type whose meaning lives in its numeric value, for a schema that must keep a stable wire format while identifiers get renamed, and for a team that cannot accept a code generator writing files into their package.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 20 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What go generate writes into pill_enumer.go

Enumer starts from an ordinary typed constant block and writes a second file beside it. The README example is the canonical case:

go
//go:generate go run github.com/dmarkham/enumer@latest -type=Pill -json
type Pill int

const (
	Placebo Pill = iota
	Aspirin
	Ibuprofen
	Paracetamol
	Acetaminophen = Paracetamol
)

Running go generate writes pill_enumer.go, lowercased from the type name, unless you pass -output. The generated file always contains five things: a String method that returns the name of the value, a TString function that parses a name back into a value, TValues returning all values in declaration order, TStrings returning all names, and an IsAT method that reports whether a value is one of the declared constants.

Two details in that contract matter more than the rest. Unknown values print as T(42), so a zero value or a corrupt one never silently reads as a real name. And aliases are skipped by TValues and TStrings, which is why the example declares Acetaminophen = Paracetamol without the name appearing twice in the lists.

The parser matches the exact name first and then the lowercase form, so PillString("ibuprofen") resolves to Ibuprofen. Enumer is described as a drop-in replacement for stringer: it generates the same String method, so existing code keeps working, and the extras are additive.

Pinning enumer as a tool dependency in go.mod

Enumer needs Go 1.25 or newer. The recommended setup pins enumer in go.mod as a tool dependency, so go generate works on a fresh clone with no separate install step:

sh
go get -tool github.com/dmarkham/enumer@latest

With the tool pinned, the directive in the source file becomes:

go
//go:generate go tool enumer -type=Pill

The repository ships a runnable module configured this way at examples/gomods, which is the fastest way to see the whole arrangement working.

Two alternatives are documented. The first runs the generator on demand from go generate without touching go.mod:

go
//go:generate go run github.com/dmarkham/enumer@latest -type=Pill

The second installs a binary on your PATH:

sh
go install github.com/dmarkham/enumer@latest

Prebuilt binaries for Linux, macOS and Windows are on the releases page if you would rather not build. The choice between the three is a real trade-off. The tool dependency gives every contributor the same generator version, but it adds enumer to your module graph and means a go.mod update is needed to move versions. The go run form keeps go.mod clean but downloads on every generate and pins nothing. A PATH binary is the lightest for CI, and the least reproducible.

The command line itself takes the type name and an optional directory or file list:

sh
enumer [flags] -type T [directory]
enumer [flags] -type T files...   # files must be in a single package

The type flag is required and accepts a comma separated list of type names.

Encoding flags decide how an enum survives a process boundary

Beyond the always generated methods, each encoding flag adds one pair of methods for one wire format. The table is short enough to state fully. -json adds MarshalJSON and UnmarshalJSON for encoding/json. -text adds MarshalText and UnmarshalText for the encoding package. -yaml adds MarshalYAML and UnmarshalYAML in gopkg.in/yaml.v2 style, which yaml.v3 also accepts. -sql adds Value and Scan for database/sql/driver. -gqlgen adds MarshalGQL and UnmarshalGQL for gqlgen.

Every one of them stores the enum as its string name, and unmarshaling an unknown name returns an error. That last point is a design choice worth naming: the default behaviour rejects garbage rather than silently producing the zero value, which is what makes the generated code usable for validation at a boundary.

The README calls out one trap. If the enum is a map key that you encode as JSON, you want -text, because without it encoding/json writes the numeric value as the key. That is a small detail that costs a debugging session if you miss it.

The Scan method is a little more forgiving: it accepts a string, a []byte, or any fmt.Stringer, and a nil value leaves the receiver unchanged rather than zeroing it.

The README also shows the intended combos. For an API that wants snake case JSON:

sh
enumer -type=Status -json -transform=snake

For a database column that should not carry a Go style prefix:

sh
enumer -type=Color -trimprefix=Color -sql -text

And for several types at once into one file:

sh
enumer -type=Pill,Day -output=enums_gen.go

Renaming is trimprefix, then transform, then addprefix

By default the string name of a constant is its Go identifier. Three flags adjust that, and the README is precise about the order: -trimprefix first, then -transform, then -addprefix. The result is what String returns, what TString accepts, and what every encoding uses, so a rename is a change to the wire format rather than a cosmetic change.

-trimprefix takes a comma separated list so several prefixes can be tried in turn, and names that do not carry the prefix are left alone. -transform rewrites the case of the name, with noop as the default and the alternatives snake, snake-upper, kebab, kebab-upper, dot, dot-upper, whitespace, lower, upper, title, title-lower, first, first-upper and first-lower.

There is a constraint on the transformations that trips people up. Word splitting only works from CamelCase, so a constant named MyTypeValue becomes my_type_value with snake but cannot be split out of an identifier that has no case boundaries to split on. The underscore spellings of the upper case variants are accepted as aliases of the hyphenated names.

A separate flag, -linecomment, replaces the name with the constant's trailing line comment:

go
const (
	Monday Day = iota // lunes
	Tuesday
	Friday // viernes
)

With that flag, Monday.String() returns lunes, Tuesday.String() returns Tuesday because it has no trailing comment, and Friday.String() returns viernes. That is the mechanism to reach for when the identifier and the value in the database are genuinely different words.

Typed errors turn a build tool into a runtime dependency

Most of what enumer generates needs nothing from the enumer package at run time. The exception is the -typederrors flag. With it, TString, Validate and the unmarshal methods return an error that matches enumerrs.ErrValueInvalid under errors.Is, while the message still names the bad input:

go
import "github.com/dmarkham/enumer/enumerrs"

p, err := PillString("Vitamin")
if errors.Is(err, enumerrs.ErrValueInvalid) {
	// "Vitamin does not belong to Pill values"
}

The README is upfront about the cost: this makes github.com/dmarkham/enumer a runtime dependency of your module, since the generated code imports enumerrs. That is the deal. Without the flag your binary carries no trace of the generator; with it you gain a stable sentinel error to branch on, at the price of a module dependency that has to be vendored, upgraded and audited like any other.

The same pattern appears in the other optional flags. -values adds Values() []string, which ent uses for enum fields. -validate adds Validate() error, which fails when the value is not a declared constant. -flag.value adds Set(string) error so the type satisfies flag.Value, and -pflag.value adds Set and Type() string for pflag.Value, where Type returns all names joined by a pipe character. -comment adds a comment line to the top of the generated file and is repeatable, which is where a reminder about not editing the file by hand belongs.

Where enumer is the wrong tool

The generated vocabulary is about names, and several things you might want an enum to do are simply not in it.

There is no numeric value mapping. The README shows the enum as an int with iota, but the generated methods never expose the underlying number, and there is no documented flag to attach an explicit value to a constant or to serialise as a number. If your wire format must stay numeric, enumer is the wrong tool, because the whole design assumes the name is the identity.

There is also no support for hierarchical or grouped enums, no flag range, and no way to mark a constant as deprecated. The parser matches an exact name and then a lowercase form, so two constants differing only in case cannot both be resolved. The -linecomment option comes closest to a second name system, but it replaces the identifier rather than adding an alias.

The generated file is committed code. It lives in your package as pill_enumer.go, it is regenerated whenever the constants change, and the repository itself is organised around that assumption, with endtoend_test.go, golden_test.go and a testdata directory. If your team has a policy against generated code in the package, or you would rather keep the generator out of the build, that is a workflow objection rather than a technical one, but it is a real one.

Finally, the tool requires Go 1.25, and the module declares go 1.25.0 with a dependency on golang.org/x/tools v0.47.0. On a team still on an older toolchain, enumer is not usable as documented.

stringer and jsonenums, and the fork history

Enumer did not appear from nowhere, and its own history section is the honest comparison. It started as a fork of Rob Pike's stringer, was extended by Álvaro López Espinosa, and continues under the current maintainer. jsonenums by campoy inspired the JSON support.

That lineage explains the positioning. stringer, still in golang.org/x/tools, does one job well: it writes a String method from a set of constants, and it comes from golang.org/x/tools, so if all you need is String and you already have x/tools available, stringer needs no extra dependency in go.mod and produces less code. Enumer earns its place when you want the rest: the parser, the values and strings lists, the IsAT check, and the encoding flags.

jsonenums is the narrower comparison on the other side. It addresses the JSON case, and enumer's -json flag borrows from it. The difference is that enumer keeps the JSON support inside one tool alongside the other encodings, so a type with SQL and gqlgen needs one generator and one generated file rather than several.

The main README also frames enumer as a drop-in replacement for stringer, generating the same String method so existing code keeps working. In practice the upgrade path is easy: run enumer over the same types, keep the String method, and add whatever you now need on top.

The repository layout shows a tool with tests behind it. The Makefile has a test target running go test with the race detector and an atomic coverage profile, plus a build-cli target that produces Linux, macOS and Windows binaries with SHA-256 sums written out. The module also depends on github.com/pascaldekloe/name and golang.org/x/tools.

Editorial conclusion

enumer is a good fit for a Go service that defines its state as typed constants and then needs those constants to survive JSON, YAML, SQL and gqlgen without writing four hand rolled marshalling files. It is the wrong tool for a type whose meaning lives in its numeric value, for a schema that must keep a stable wire format while identifiers get renamed, and for a team that cannot accept a code generator writing files into their package. Verify first by generating into a throwaway directory with -output and reading the diff, because the generated file becomes part of your module and, with -typederrors, the generated code imports github.com/dmarkham/enumer/enumerrs, which turns a build time tool into a runtime dependency.

Frequently asked questions

How do I install enumer so go generate works on a fresh clone?

Run go get -tool github.com/dmarkham/enumer@latest to pin it in go.mod as a tool dependency, then use the directive //go:generate go tool enumer -type=Pill in your source. The repository includes a runnable module set up this way at examples/gomods.

What does enumer generate for a typed constant block?

It always writes a String method, a TString parser, TValues and TStrings, and an IsAT check, into a file named after the type in lowercase, such as pill_enumer.go. Encoding flags add MarshalJSON, MarshalText, MarshalYAML, database/sql Value and Scan, or gqlgen methods on top.

Is enumer a replacement for stringer?

Yes. The README describes enumer as a drop-in replacement that generates the same String method as stringer plus extra methods, so existing code keeps working. Enumer began as a fork of Rob Pike's stringer, and stringer remains the simpler option if String is all you need.

Does using the -typederrors flag add a dependency to my module?

Yes. The README states that typed errors make github.com/dmarkham/enumer a runtime dependency of your module, because the generated code imports the enumerrs package. Without the flag the generated code does not import the generator.

Can enumer change the string name of a constant?

Yes, with three flags applied in order: -trimprefix removes a prefix, -transform rewrites the case with values such as snake, kebab, dot or lower, and -addprefix prepends a string. The result becomes what String returns and what every encoding stores.

What Go version does enumer require?

Enumer needs Go 1.25 or newer, and the module declares go 1.25.0. It depends on golang.org/x/tools v0.47.0 and github.com/pascaldekloe/name v1.0.0.

Official sources

  1. dmarkham/enumer on GitHub
  2. Issues
  3. README
  4. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/dmarkham-enumer.svg)](https://hysenlabs.com/projects/dmarkham-enumer)

Was this useful?