Edict is a zero-dependency validation library for Go. Declare rules with a fluent builder API, execute them in a single Validate() call, and get structured errors back. Because every field value is registered before any rule runs, cross-field comparisons and conditional requirements work without special wiring. Works with maps, structs, and direct values.
Three data modes
- Map mode: pass a
map[string]anyfrom your YAML/JSON parser, validate with dot-path field lookups - Struct mode: resolve values from exported struct fields via reflection
- Direct-value mode: provide values explicitly per field
30+ built-in rules
- Presence:
Required,OneOf,NotOneOf - Numeric:
IntRange,IntMin,IntMax,FloatRange,FloatMin,FloatMax,Maxwith automatic type coercion across int/float/json.Number variants - String length:
MinLength,MaxLength,LenRange,LenExact(Unicode aware) - Format validators:
IsEmail,IsURL,IsUUID,IsSlug,IsSemver,IsHexColor,IsAlpha,IsAlphaNumeric,IsNumeric,IsIPv4,IsIPv6
Cross-field rules
- Comparison:
LessOrEqual,EqField,GteField,LteField(numeric via float64, string fallback) - Conditional required:
RequiredIf,RequiredUnless,RequiredWith,RequiredWithAll,RequiredWithout,RequiredWithoutAll - Exclusion:
ExcludedIf,ExcludedUnless
Collection validation
- Dive into slices, arrays, and maps with
Each(),EachKey(),EachValue() - Wildcard paths:
v.Field("servers.*.port")validates a sub-field across all elements Distinct()for duplicate detection- Nested dives for multidimensional collections
Error model
- Errors and warnings as separate concerns with per-rule severity
- Structured access:
Path,Code,Value,Message,Severity - Deterministic output in field declaration order
- Internal errors for infrastructure failures in custom rules
Extensibility
Ruleinterface,RuleFunc,Check(),CheckIf()ResolverAwarefor custom cross-field rulesRuleGroupfor reusable rule setsWithMessagesregistry for i18n
go get github.com/frostybee/edictRequires Go 1.25+.
Ideal for YAML/JSON config data parsed into map[string]any. Supports nested maps via dot-path traversal.
data := map[string]any{
"port": 99999,
"format": "xml",
}
v := edict.FromMap(data)
v.Field("port").Required().IntRange(1, 65535)
v.Field("format").OneOf("json", "csv", "table")
if err := v.Validate(); err != nil {
fmt.Println(err)
// port: must be between 1 and 65535 (got 99999)
// format: must be one of: json, csv, table (got "xml")
}The caller provides values explicitly. Useful when validating struct fields or standalone variables.
v := edict.New()
v.Field("port").Value(cfg.Port).Required().IntRange(1, 65535)
v.Field("title").Value(cfg.Title).Required()
if err := v.Validate(); err != nil {
fmt.Println(err)
}Values resolved from exported struct fields via reflection. Paths use Go field names, not struct tags.
v := edict.FromStruct(&cfg)
v.Field("Port").Required().IntRange(1, 65535)
v.Field("DarkMode.Kind").OneOf("selector", "mediaQuery")
if err := v.Validate(); err != nil {
fmt.Println(err)
}All three modes share the same rules and produce identical output. Cross-field rules work in every mode.
Validating a static site generator config with hard errors and soft warnings:
func Validate(cfg *SiteConfig) ([]edict.Error, error) {
v := edict.New()
v.Field("site.title").Value(cfg.Site.Title).Required()
v.Field("site.language").Value(cfg.Site.Language).Required()
v.Field("server.port").Value(cfg.Server.Port).IntRange(1, 65535)
v.Field("toc.min_level").Value(cfg.TOC.MinLevel).IntRange(1, 6)
v.Field("toc.max_level").Value(cfg.TOC.MaxLevel).IntRange(1, 6)
v.Field("toc.min_level").LessOrEqual("toc.max_level")
v.Field("deploy.provider").Value(cfg.Deploy.Provider).
OneOf("netlify", "vercel", "cloudflare", "github-pages")
v.Warn("site.url").Value(cfg.Site.URL).Required()
v.Warn("site.description").Value(cfg.Site.Description).Required()
err := v.Validate()
return v.Warnings(), err
}Pass options when creating a validator:
v := edict.FromMap(data, edict.StopOnFirst(), edict.StrictFields())| Option | Effect |
|---|---|
StopOnFirst() |
Stop after the first error (warnings don't trigger the stop) |
StrictFields(known...) |
Report unknown keys as errors. No args = auto-detect from declared fields |
WithMessages(registry) |
Register global message overrides by rule code (i18n) |
Restrict which fields are validated. Excluded fields still contribute values for cross-field rule lookups.
v.Only("port", "format") // validate only these fields
v.Except("deprecated_field") // skip this fieldChain after any rule to change its behavior:
v.Field("port").IntMin(1024).AsWarning()
v.Field("format").OneOf("json", "csv").Message("unsupported output format")| Modifier | Effect |
|---|---|
AsWarning() |
Changes severity of the last rule to warning |
Message(msg) |
Overrides the error message of the last rule |
OmitEmpty() |
Skips rules when value is zero (default behavior, makes intent explicit) |
OmitNil() |
Skips rules only when value is nil; zero values like 0, "", false still validate |
Rules default to SeverityError. Use v.Warn("path") to start a chain where all rules default to SeverityWarning, or .AsWarning() to change just the last added rule.
Validate() returns nil on success. On failure, it returns a *Results that implements error:
var results *edict.Results
if errors.As(err, &results) {
for _, e := range results.Errors() {
fmt.Printf("field=%s code=%s msg=%s\n", e.Path, e.Code, e.Message)
}
}Warnings never affect the error return. Access them via v.Warnings() after calling Validate().
Override error messages globally by rule code. Per-field .Message() overrides take precedence.
v := edict.FromMap(data, edict.WithMessages(edict.MessageRegistry{
"required": func(e edict.Error) string {
return fmt.Sprintf("%s is a required field", e.Path)
},
}))go build ./...
go test ./...
go test -run TestSpecificName ./...
go vet ./...No Makefile, no task runner. Standard Go toolchain only.
- Fork and create a feature branch.
- Run
go test ./...before submitting. - Keep changes focused. One feature or fix per PR.
When adding a new rule, include thorough tests covering: boundary values, zero-value types (nil, "", 0, false), wrong types, error message content, and error code assertions. See existing rules_*_test.go files for the expected depth.
Copyright (c) 2026 FrostyBee.
Edict is licensed under the MIT License.
Full API documentation: pkg.go.dev/github.com/frostybee/edict