Everything you need to go from install to a validated document, then extend the library with your own rules.
Requires Go 1.24 or newer. There are no third-party dependencies.
go get github.com/ashbeelghouri/json-schematics-v2@latest
A schema is data. Each field targets a flattened key and lists the validate rules and operate steps to run against it.
{
"version": "2.0",
"fields": [
{
"target": "user.profile.name",
"required": true,
"validate": [
{ "rule": "isString" },
{ "rule": "minLength", "args": { "min": 2 } }
],
"operate": [ { "op": "trim" }, { "op": "capitalize" } ]
}
]
}Validate returns nil when the document is valid, a *ValidationErrors for data problems, or a *SchemaError if the schema references a rule that isn't registered. It never panics.
s := schematics.New() if err := s.LoadFile("schema.json"); err != nil { log.Fatal(err) } if err := s.Validate(payload); err != nil { var ve *schematics.ValidationErrors if errors.As(err, &ve) { for _, m := range ve.Strings("en", "%target: %message") { fmt.Println(m) } } }
A target selects one or more flattened keys. Use a literal path, a * wildcard for a single segment (array indices), or a full regex with targetRegex: true.
user.profile.nameexact literal pathmatchesuser.profile.nameitems.*.skuwildcard, one segmentmatchesitems.0.sku, items.1.sku^user\.(name|email)$regex, targetRegex: truematchesuser.name, user.emailOperators transform values in a pass separate from validation. Call Operate to get the reshaped document back.
out, err := s.Operate(payload) // out is the transformed document: // " ada " -> trim -> "ada" -> capitalize -> "Ada"
Register typed functions before you validate. Each receives the value, the schema args, and a *Context with the shared DB. Return an error to fail, never a panic. Reference them in the schema by name.
s.RegisterRule("isSlug", func(v any, a schematics.Args, _ *schematics.Context) error { str, ok := v.(string) if !ok || !slugRe.MatchString(str) { return fmt.Errorf("%q is not a slug", v) } return nil })
s.RegisterOperator("slugify", func(v any, a schematics.Args, _ *schematics.Context) (any, error) { str, _ := v.(string) return strings.ReplaceAll(strings.ToLower(str), " ", "-"), nil })
Every rule, operator, and condition receives a *Context. It carries the active locale and separator, the whole flattened document, the current array RowID, a Go context.Context, and a DB map you can use as shared memory across fields.
type Context struct { Ctx context.Context // cancellation for slow custom rules DB map[string]any // shared memory Locale string Flat map[string]any // the whole flattened document RowID string // array row id, empty for objects }
Fill it with global config or request-scoped data from Go, or from the schema itself. Values are available to every field's rules.
s := schematics.New(schematics.WithDB(map[string]any{ "minAge": 18, "allowedRoles": []any{"admin", "editor"}, }))
Or copy a value out of the data itself with addToDB. It is stored under the field's target key before validation runs, so any other field can read it.
{ "target": "password", "addToDB": true,
"validate": [ { "rule": "minLength", "args": { "min": 8 } } ] }Read a sibling field with ctx.Lookup(target) (no addToDB needed), or read seeded config with ctx.DB[key]. This is how you express confirm-password, date ordering, or "this depends on that field's value".
s.RegisterRule("matchesField", func(v any, a schematics.Args, ctx *schematics.Context) error { other, _ := a.String("field") want, ok := ctx.Lookup(other) if !ok || fmt.Sprintf("%v", v) != fmt.Sprintf("%v", want) { return fmt.Errorf("must match %s", other) } return nil })
{ "target": "confirm",
"validate": [ { "rule": "matchesField", "args": { "field": "password" } } ] }DB is built per document, so array rows never leak state into each other. Treat it as read-mostly: prefer Lookup or addToDB over mutating ctx.DB inside a rule, since rule order within a field is first-fail-wins. For slow rules that call a database or remote service, honor ctx.Ctx.Done() and run with s.ValidateCtx(ctx, data).
Attach a messages map to any rule for per-locale strings, then render them with ve.Strings("ar", ...). See the localized errors example.