Skip to content

frostybee/edict

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Edict

Go Reference CI License: MIT Go Version

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.

Features

Three data modes

  • Map mode: pass a map[string]any from 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, Max with 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

  • Rule interface, RuleFunc, Check(), CheckIf()
  • ResolverAware for custom cross-field rules
  • RuleGroup for reusable rule sets
  • WithMessages registry for i18n

Install

go get github.com/frostybee/edict

Requires Go 1.25+.

Quick start

Map mode

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")
}

Direct-value mode

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)
}

Struct mode

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.

A real-world example

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
}

Configuration

Validator options

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)

Field filtering

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 field

Modifiers

Chain 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

Severity

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.

Error handling

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().

Message registry

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)
    },
}))

Development

go build ./...
go test ./...
go test -run TestSpecificName ./...
go vet ./...

No Makefile, no task runner. Standard Go toolchain only.

Contributing

  1. Fork and create a feature branch.
  2. Run go test ./... before submitting.
  3. 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.

License

Copyright (c) 2026 FrostyBee.

Edict is licensed under the MIT License.


Full API documentation: pkg.go.dev/github.com/frostybee/edict

About

A zero-dependency validation library for Go.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages