Strongly-typed string enums for Go using phantom-type generics.
Two wire formats supported out of the box:
- Raw:
"value" - Qualified:
"Enum.value"(pretty and self-describing)
Batteries included: JSON/Text marshal/unmarshal, database/sql Scan/Value, a tiny registry, helpers (All, Contains, Qualified, …), and examples.
Requires Go 1.18+ (generics). Recommended: 1.20+.
go get github.com/JasonAcar/strenum@latest
# or pin a release
go get github.com/JasonAcar/strenum@v0.1.0Import:
import "github.com/JasonAcar/strenum"package yourpkg
import "github.com/JasonAcar/strenum"
// 1) Define a unique phantom tag type for this enum:
type ProjectTypeTag struct{}
// 2) (Optional) Alias a friendly field type:
type ProjectType = strenum.Enum[ProjectTypeTag]
// 3) Declare the spec once (registers itself):
var ProjectTypeSpec = strenum.NewStrongEnum[ProjectTypeTag](
"ProjectType", "go", "node", "python",
)
// 4) Use it in your structs:
type Project struct {
ProjectType ProjectType `json:"project_type"`
Repo string `json:"repo"`
}Behavior:
v := ProjectTypeSpec.Must("go")
fmt.Println(v) // ProjectType.go
fmt.Println(v.Raw()) // go
fmt.Println(v.Qualified())// ProjectType.go
fmt.Println(v.IsValid()) // trueJSON round-trip:
p := Project{ProjectType: ProjectTypeSpec.Must("node")}
b, _ := json.Marshal(p) // {"project_type":"ProjectType.node"}
var p2 Project
_ = json.Unmarshal([]byte(`{"project_type":"ProjectType.go"}`), &p2) // okPrefer plain strings on the wire? Use NewEnum:
type MessageStatusTag struct{}
type MessageStatus = strenum.Enum[MessageStatusTag]
var MessageStatusSpec = strenum.NewEnum[MessageStatusTag](
"MessageStatus", "queued", "delivered", "failed",
)
m := MessageStatusSpec.Must("queued")
fmt.Println(m) // queued
fmt.Println(m.Raw()) // queued
type Message struct {
Status MessageStatus `json:"status"`
}
b, _ := json.Marshal(Message{Status: m}) // {"status":"queued"}Parsing accepts raw or qualified input in both modes:
_ = json.Unmarshal([]byte(`{"status":"delivered"}`), &msg) // ok
_ = json.Unmarshal([]byte(`{"status":"MessageStatus.delivered"}`), &msg) // also okProjectTypeSpec.Contains("go") // true
ProjectTypeSpec.AllStrings() // []string{"go","node","python"}
for _, v := range ProjectTypeSpec.All() {
fmt.Println(v, v.Raw(), v.IsValid())
}Values store/read as their String():
var v ProjectType
_ = v.Scan("ProjectType.go") // ok
s, _ := v.Value() // "ProjectType.go"
var raw MessageStatus
_ = raw.Scan([]byte("queued")) // ok
s2, _ := raw.Value() // "queued"type EnumFlag[T any] struct {
Spec *strenum.Spec[T]
Value strenum.Enum[T]
}
func (f *EnumFlag[T]) String() string { return f.Value.String() }
func (f *EnumFlag[T]) Set(s string) error {
v, err := f.Spec.Parse(s); if err != nil { return err }
f.Value = v; return nil
}
// usage
var pt EnumFlag[ProjectTypeTag] = EnumFlag[ProjectTypeTag]{Spec: ProjectTypeSpec}
flag.Var(&pt, "project-type", "one of: "+strings.Join(ProjectTypeSpec.AllStrings(), ", "))Accepts -project-type go or -project-type ProjectType.go.
Every spec registers itself (keyed by the phantom tag type). You can list them for diagnostics:
for _, e := range strenum.RegistryEntries() {
fmt.Println(e)
// Example: ProjectTypeTag → ProjectType (qualified) [go node python]
}- You create a zero-size phantom tag type per enum (e.g.,
ProjectTypeTag). - Your field type is
Enum[ProjectTypeTag]. - A
Spec[ProjectTypeTag]defines valid values and is auto-registered. Enum[T]methods consult the registeredSpec[T]—no parent pointers.
Wire format is decided by constructor:
NewEnum→String()/JSON emit raw ("value").NewStrongEnum→ emit qualified ("Enum.value").
Parse/unmarshal accept raw or qualified input for convenience.
Thread-safety: Spec is immutable after construction; registry uses sync.Map. Safe to share across goroutines.
_, err := ProjectTypeSpec.Parse("ProjectType.rust")
// err: "rust is not a valid ProjectType (valid: [go node python])"
_, err = ProjectTypeSpec.Parse("WrongEnum.go")
// err: `enum "WrongEnum" does not match "ProjectType"`On JSON/Text/SQL decode, the same validations are applied.
.
├── go.mod # module github.com/JasonAcar/strenum
├── doc.go # package comment (optional)
├── generic.go # the library (package strenum)
├── generic_test.go # examples & tests (pkg strenum_test/strenum)
└── examples/
└── main.go # runnable demos (package main)
Run everything:
go test ./...
go run ./examplesExamples on pkg.go.dev: they define the tag + spec inside each Example… function so the snippet is self-contained. In real code, put them at package scope.
Classic iota + String() lets any int sneak in. This gives you:
- stringy ergonomics,
- compile-time type separation per enum,
- clear wire format,
- built-in JSON/SQL support.
Specs are intended to be immutable.
No—pick per enum. Many APIs prefer raw (NewEnum), internal logs prefer qualified (NewStrongEnum).
Enum[T] still marshals to its raw string. On unmarshal/parse, behavior depends on whether a Spec[T] is registered: if found, it validates; otherwise it accepts the raw string.
This module follows semver. For major versions v2+, Go requires a path suffix:
module github.com/JasonAcar/strenum/v2
import "github.com/JasonAcar/strenum/v2"MIT, See LICENSE.
- Docs: https://pkg.go.dev/github.com/JasonAcar/strenum
- Issues/PRs: https://github.com/JasonAcar/strenum
// Qualified example
type PTTag struct{}
type ProjectType = strenum.Enum[PTTag]
var ProjectTypeSpec = strenum.NewStrongEnum[PTTag]("ProjectType", "go", "node")
// Raw example
type MSTag struct{}
type MessageStatus = strenum.Enum[MSTag]
var MessageStatusSpec = strenum.NewEnum[MSTag]("MessageStatus", "queued", "delivered")
type S struct{ T ProjectType `json:"t"` }
type M struct{ Status MessageStatus `json:"status"` }
func demo() {
// Parse
fmt.Println(ProjectTypeSpec.Must("go")) // ProjectType.go
// JSON
b, _ := json.Marshal(S{T: ProjectTypeSpec.Must("node")})
fmt.Println(string(b)) // {"t":"ProjectType.node"}
var m M
_ = json.Unmarshal([]byte(`{"status":"delivered"}`), &m)
fmt.Println(m.Status) // delivered
}