schematics

package module
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 24, 2026 License: MIT Imports: 20 Imported by: 0

README

json-schematics-v2

Go Reference Go Report Card CI MIT License

Validate and transform arbitrary JSON in Go using a small, declarative schema — with no third-party dependencies.

📖 Full documentation, live examples, and an interactive playground: jsonschematics.ashbeelghouri.com

json-schematics-v2 flattens any document into dotted keys (user.profile.name, tags.0), lets each schema field target one or more of those keys (literally, with a * wildcard, or with a regular expression), and runs every matched value through an ordered chain of validators and operators.

This is a ground-up redesign of the original jsonschematics. See MIGRATION.md for what changed and why.

Features

  • 🎯 Target anything — match flattened keys literally, with a * wildcard, or a full regex.
  • 🧩 Batteries included — 56 validators, 18 operators, and 7 conditions built in.
  • 🔒 Concurrency-safe — one *Schematics (or *API) can be built once and shared across goroutines; compiled matchers are cached, so per-request validation is allocation-light.
  • 🏗️ Fluent builder — define schemas in Go with NewSchema().Field(...).Required().Email(), so a mistyped rule or target is a compile error, not a runtime surprise.
  • 🛡️ Never panics — wrong types return typed errors; unknown rule names are caught up front by Check().
  • 🔍 Catches target typosValidateSchema() matches every field's target against sample data, so a typo like "target": "mane" fails your tests instead of silently matching nothing in production.
  • 🧪 Type-aware — opt into WithTypeChecks() and a field's type is enforced; WithCollectAll() reports every failure per field.
  • 🌍 Localized errors — per-locale messages and format templates; ValidationError marshals to clean JSON.
  • ⚙️ Validate and transform — a separate operator pass, plus a shared context DB for cross-field logic.
  • 🌐 HTTP request validation — validate headers, query, and body per endpoint, with a body-size cap.
  • 📦 Zero dependencies — standard library only, Go 1.24+.

Install

go get github.com/ashbeelghouri/json-schematics-v2@latest
import schematics "github.com/ashbeelghouri/json-schematics-v2"

Quick start

s := schematics.New()
if err := s.LoadFile("schema.json"); err != nil {
    log.Fatal(err)
}

data := map[string]any{
    "user": map[string]any{"profile": map[string]any{"name": "a", "age": 200}},
}

if err := s.Validate(data); err != nil {
    var ve *schematics.ValidationErrors
    if errors.As(err, &ve) {
        for _, msg := range ve.Strings("en", "%target: %message") {
            fmt.Println(msg)
        }
    }
}

Validate returns:

  • nil when the data is valid,
  • a *ValidationErrors when the data breaks the rules,
  • a *SchemaError when the schema references an unknown rule/operator/condition.

Use errors.As to tell them apart.

Working with raw bytes

ImportSchema and ValidateBytes skip the manual json.Unmarshal step when you already have schema and data as []byte — a config file, a request body, an embedded fixture:

s, err := schematics.ImportSchema(schemaBytes) // New + LoadBytes in one call
if err != nil {
    log.Fatal(err)
}

// isArray tells ValidateBytes whether dataBytes is a single JSON object or
// an array of objects — pass it explicitly instead of relying on shape-
// sniffing, so a mismatched payload fails with a clear parse error.
if err := s.ValidateBytes(dataBytes, false /* isArray */); err != nil {
    // *ValidationErrors, *SchemaError, or a JSON parse error
}

ValidateBytesCtx takes a context.Context the same way ValidateCtx does.

The schema

{
  "version": "2.0",
  "separator": ".",
  "arrayIdKey": "id",
  "locale": "en",
  "db": { "minAge": 18 },
  "fields": [
    {
      "target": "user.profile.name",
      "type": "string",
      "required": true,
      "dependsOn": ["user.profile.email"],
      "addToDB": false,
      "when":     [ { "condition": "fieldPresent", "args": { "field": "user.profile.email" } } ],
      "validate": [ { "rule": "minLength", "args": { "min": 2 }, "message": "too short", "messages": { "ar": "قصير جدا" } } ],
      "operate":  [ { "op": "trim" }, { "op": "capitalize" } ]
    }
  ]
}
Field keys
Key Meaning
target Flattened key to match. Literal, * wildcard (one segment), or a regex when targetRegex is true.
targetRegex Treat target as a Go regular expression.
required Fail if the target selects no value.
dependsOn Other targets that must be present for this field to be validated.
addToDB Copy the matched value into the shared db before validation.
when Conditions that must all hold for the field to run.
validate Ordered validators. The first failing rule reports one error and stops that field.
operate Ordered operators, applied by Operate.
name, displayName, type, description, tags, meta Metadata; carried through untouched.
Targeting arrays

Flattening turns {"tags":["a","b"]} into tags.0, tags.1. To validate the array itself (length, uniqueness), target the parent key — the engine reconstructs the array for you:

{ "target": "tags", "validate": [ { "rule": "minItems", "args": { "min": 1 } }, { "rule": "unique" } ] }

To validate each element, use a wildcard: "target": "tags.*".

Validating an array of objects? Pass a []map[string]any (or JSON array) to Validate; set arrayIdKey so each error carries a RowID.

Built-in validators

Strings: isString, notEmpty, email, maxLength, minLength, lengthBetween, exactLength, noSpecialChars, hasSpecialChars, hasUpper, hasLower, hasDigit, alpha, alphanumeric, contains, startsWith, endsWith, isURL, notURL, urlHasHost, urlHasQuery, isHTTPS, isUUID, isIP, isCIDR, isJSON, isBase64, isHex, equals, inOptions, notInOptions, matchRegex, like.

Numbers: isNumber, isInteger, isFloat, max, min, between, positive, negative, nonNegative, multipleOf.

Types: isBoolean, isObject (also used by WithTypeChecks).

Dates: isDate, beforeNow, afterNow, before, after, betweenTime.

Arrays: isArray, maxItems, minItems, unique, itemsInOptions.

Every validator inspects its input safely — the wrong type returns an error, it never panics.

Built-in operators

trim, capitalize, upper, lower, toString, add, subtract, multiply, divide, round, default, arrayToObject, replace, slugify, truncate, split, join, coerce.

out, err := s.Operate(data) // returns the transformed document

Built-in conditions

fieldPresent, fieldAbsent, fieldEquals, fieldMatches (regex), fieldGreaterThan, fieldLessThan, fieldIn — used in a field's when list. Set "negate": true on a condition to invert it.

Custom rules, operators, conditions

Signatures are typed and context-aware. Register your own before validating:

s.RegisterRule("isAsh", func(v any, args schematics.Args, ctx *schematics.Context) error {
    if v == "ash" {
        return nil
    }
    return errors.New("must be ash")
})

s.RegisterOperator("reverse", func(v any, args schematics.Args, ctx *schematics.Context) (any, error) {
    str, _ := v.(string)
    r := []rune(str)
    for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }
    return string(r), nil
})

Args provides panic-free typed accessors: args.String("k"), args.Int("k"), args.Float("k"), args.Bool("k"), args.Strings("k"), plus args.StringOr / args.FloatOr. Context exposes the shared DB, Locale, Separator, the full flattened document Flat, the current RowID, a read-only Field view, and ctx.Ctx (a context.Context) for cancellation.

Configuration

s := schematics.New(
    schematics.WithSeparator("."),
    schematics.WithLocale("en"),
    schematics.WithArrayIDKey("id"),
    schematics.WithDB(map[string]any{"minAge": 18}),
    schematics.WithLogger(slog.Default()),
    schematics.WithTypeChecks(),          // enforce each field's "type"
    schematics.WithCollectAll(),          // report every failing rule per field
    schematics.WithMaxBodyBytes(1 << 20), // cap API request bodies at 1 MiB
)

Options take precedence over values set inside the schema file.

Building schemas in Go

Prefer defining a schema in code over hand-writing JSON? The fluent builder gives you editor autocomplete and compile-time checking of rule and field names, then emits an ordinary Schema:

s, err := schematics.NewSchema().
    Field("email").Required().Type("string").Email().
    Field("age").Type("integer").Min(18).Max(120).
    Field("name").MinLength(2).Trim().Capitalize().
    New(schematics.WithTypeChecks())
if err != nil {
    log.Fatal(err) // New runs Check, so an unknown rule fails here
}

Every built-in has a typed helper (Email(), MinLength(n), IsIP(), …), and Rule(name, args) / Op(name, args) / When(cond, args) are escape hatches for custom or less common ones. .Schema() returns the Schema value if you'd rather apply it yourself.

Enforcing types and collecting every error

By default a field's type is documentation. Turn on WithTypeChecks() and each field's type (string, number, integer, boolean, array, date, object) is enforced before its own validators run. Turn on WithCollectAll() and each field reports every failing rule instead of stopping at the first — ideal for form validation:

s := schematics.New(schematics.WithCollectAll())
_ = s.LoadBytes([]byte(`{"fields":[{"target":"pw","validate":[
    {"rule":"minLength","args":{"min":8}},{"rule":"hasUpper"},{"rule":"hasDigit"}]}]}`))
// "ab" -> three errors, not one

Concurrency

A *Schematics and an *API are safe for concurrent use once the schema is loaded and any custom rules are registered. Build one at start-up and share it across request-handling goroutines — validation takes only read locks and reuses a cached, compiled matcher for every wildcard/regex target. Registering rules or loading a new schema must happen during setup, before the value is shared.

Key/separator note: flattening joins keys with the separator (. by default). If your documents can contain object keys that include the separator, pick one that can't occur in your keys via WithSeparator — otherwise the flatten/deflate round-trip is ambiguous for those keys.

Error handling

var ve *schematics.ValidationErrors
if errors.As(err, &ve) {
    ve.Strings("en", "%target: %message") // []string, formatted
    ve.Messages("ar")                      // []string, localized messages only
    ve.ForTarget("user.profile.name")      // []*ValidationError
    for _, e := range ve.Errors {
        _ = e.Target // "user.profile.name"
        _ = e.Rule   // "minLength"
        _ = e.Value  // the offending value
        _ = e.RowID  // array row id, if any
        _ = e.Message("ar")
    }
}

Format tokens: %message, %target, %rule (alias %validator), %value, %id. Each *ValidationError also marshals to a stable JSON object.

Catching target typos before they ship

Because a schema is data, not code, a typo in a field's target doesn't fail to compile — it just quietly matches nothing at runtime. Check() (run automatically before every Validate) catches a typo in a JSON key, like "tagret" instead of "target": encoding/json drops the unrecognized key, Target ends up "", and Check reports "field #N has an empty target".

A typo in the target's value"target": "mane" instead of "name" — is a perfectly valid string, so Check alone has nothing to flag. ValidateSchema closes that gap by matching every target against a sample of your real data:

s := schematics.New()
s.LoadFile("schema.json")

sample := map[string]any{"name": "Ada", "email": "ada@example.com"}
if err := s.ValidateSchema(sample); err != nil {
    log.Fatal(err) // *SchemaError: field #0: target "mane" does not match anything in the sample data
}

Run it once in a test alongside a fixture (or a golden payload) so schema/data drift fails CI instead of failing silently in production:

func TestSchemaMatchesFixture(t *testing.T) {
    s := schematics.New()
    if err := s.LoadFile("schema.json"); err != nil {
        t.Fatal(err)
    }
    if err := s.ValidateSchema(loadFixture(t)); err != nil {
        t.Fatal(err) // schema drifted from the real payload shape — fix the typo
    }
}

If a field is legitimately optional and absent from every fixture you have, pass its target to ignoreTargets so it isn't flagged:

s.ValidateSchema(sample, "user.profile.middleName")

ValidateSchema runs every check Check does, so it also still catches unknown rule/operator/condition names and duplicate targets.

HTTP request validation

Validate *http.Request headers, query, and body against per-endpoint field sets:

api := schematics.NewAPI()
if err := api.LoadFile("api.schema.json"); err != nil {
    log.Fatal(err)
}
// api.Base().RegisterRule(...) to add custom rules used by the request schema
if err := api.ValidateRequest(r); err != nil {
    // *ValidationErrors or *SchemaError
}

Endpoint paths support :name (one segment) and a trailing * wildcard. See examples/api.schema.json.

Examples

Runnable schema and data live in examples/. The package-level Example in the tests loads them end to end. Try any schema live in the playground.

Development

go test ./...
go test -race -cover ./...
go vet ./...
go test -bench=. -benchmem ./...
go test -run=^$ -fuzz=FuzzValidate -fuzztime=30s ./...

License

MIT — see LICENSE.

Documentation

Overview

Package schematics validates and transforms arbitrary JSON documents against a declarative, data-driven schema.

The model is simple: a document is flattened into dotted keys (for example user.profile.name or tags.0), each schema field targets one or more of those keys (literally, with a * wildcard, or with a full regular expression), and every matched value is run through a chain of validators and operators. Validators report problems as typed ValidationError values; operators transform the value in place.

The package has no third-party dependencies.

Example

Example loads the bundled person schema and prints the validation errors for an invalid document. Errors are reported in schema field order.

package main

import (
	"errors"
	"fmt"

	schematics "github.com/ashbeelghouri/json-schematics-v2"
)

func main() {
	s := schematics.New()
	if err := s.LoadFile("examples/person.schema.json"); err != nil {
		panic(err)
	}

	data := map[string]any{
		"user": map[string]any{
			"profile": map[string]any{"name": "a", "age": 200},
			"tags":    []any{"x", "x"},
		},
	}

	if err := s.Validate(data); err != nil {
		var ve *schematics.ValidationErrors
		if errors.As(err, &ve) {
			for _, msg := range ve.Strings("en", "%target: %message") {
				fmt.Println(msg)
			}
		}
	}
}
Output:
user.profile.name: name is too short
user.profile.age: age must be 0-120
user.tags: items must be unique (duplicate x)

Index

Examples

Constants

View Source
const Version = "2.0"

Version is the schema/library generation this package implements.

Variables

This section is empty.

Functions

func Deflate

func Deflate(flat map[string]any, separator string) map[string]any

Deflate reconstructs a nested structure from a flattened map. Segments that are consecutive integers starting at zero are rebuilt as slices.

func Flatten

func Flatten(data map[string]any, separator string) map[string]any

Flatten converts a nested map into a single-level map whose keys are the paths to each leaf value, joined by separator. Arrays become indexed keys, so {"tags":["a","b"]} becomes {"tags.0":"a","tags.1":"b"}. Empty maps and slices are preserved as leaf values so length-style validators can still see them.

Flatten/Deflate round-trip losslessly only when object keys do not contain the separator and are non-empty. A literal key like "a.b" is indistinguishable from nested {"a":{"b":...}} once flattened, so if your documents can carry keys containing "." (the default separator), choose a separator that cannot occur in your keys via WithSeparator.

Types

type API

type API struct {
	// contains filtered or unexported fields
}

API validates *http.Request values against an APISchema. It reuses a base Schematics for its registries and options, so custom rules registered there are available to request validation too.

func NewAPI

func NewAPI(opts ...Option) *API

NewAPI creates an API validator with the given options (shared with the underlying engine).

func (*API) Base

func (a *API) Base() *Schematics

Base exposes the underlying Schematics so callers can register custom rules, operators, and conditions used by the request schema.

func (*API) LoadBytes

func (a *API) LoadBytes(b []byte) error

LoadBytes parses an API schema from JSON.

func (*API) LoadFile

func (a *API) LoadFile(path string) error

LoadFile reads and parses an API schema file.

func (*API) ValidateRequest

func (a *API) ValidateRequest(r *http.Request) error

ValidateRequest validates r against the endpoint matching its method and path. It returns nil, a *ValidationErrors, or a *SchemaError. If no endpoint matches, it returns a *ValidationErrors describing the mismatch.

type APIEndpoint

type APIEndpoint struct {
	Path    string  `json:"path"`
	Method  string  `json:"method"`
	Headers []Field `json:"headers,omitempty"`
	Query   []Field `json:"query,omitempty"`
	Body    []Field `json:"body,omitempty"`
}

APIEndpoint validates one method+path combination.

type APIGlobal

type APIGlobal struct {
	Headers []Field `json:"headers,omitempty"`
}

APIGlobal holds fields applied to every endpoint (currently headers).

type APISchema

type APISchema struct {
	Version   string        `json:"version,omitempty"`
	Separator string        `json:"separator,omitempty"`
	Global    APIGlobal     `json:"global,omitempty"`
	Endpoints []APIEndpoint `json:"endpoints"`
}

APISchema describes request validation for a set of HTTP endpoints. Each endpoint validates its headers, query parameters, and JSON body using the same Field model as the core schema.

Example (JSON):

{
  "version": "2.0",
  "global": { "headers": [ { "target": "authorization", "required": true } ] },
  "endpoints": [
    {
      "path": "/users/:id",
      "method": "POST",
      "query":  [ { "target": "verbose", "validate": [ { "rule": "inOptions", "args": { "options": ["true","false"] } } ] } ],
      "body":   [ { "target": "email", "required": true, "validate": [ { "rule": "email" } ] } ]
    }
  ]
}

type Args

type Args map[string]any

Args holds the arguments a schema passes to a validator, operator, or condition. The typed accessors never panic: a missing key or a wrong type is reported as an error instead, which is what makes built-in and custom rules safe to run against arbitrary data.

func (Args) Bool

func (a Args) Bool(key string) (bool, error)

Bool returns the boolean argument at key.

func (Args) Float

func (a Args) Float(key string) (float64, error)

Float returns the numeric argument at key as a float64.

func (Args) FloatOr

func (a Args) FloatOr(key string, def float64) float64

FloatOr returns the numeric argument at key, or def if absent or mistyped.

func (Args) Get

func (a Args) Get(key string) (any, bool)

Get returns the raw argument at key.

func (Args) Has

func (a Args) Has(key string) bool

Has reports whether key is present.

func (Args) Int

func (a Args) Int(key string) (int, error)

Int returns the numeric argument at key truncated to an int.

func (Args) String

func (a Args) String(key string) (string, error)

String returns the string argument at key.

func (Args) StringOr

func (a Args) StringOr(key, def string) string

StringOr returns the string argument at key, or def if absent or mistyped.

func (Args) Strings

func (a Args) Strings(key string) ([]string, error)

Strings returns the argument at key as a slice of strings.

type Condition

type Condition func(args Args, ctx *Context) bool

Condition decides whether a field's validators and operators should run.

type ConditionRef

type ConditionRef struct {
	Condition string `json:"condition"`
	Args      Args   `json:"args,omitempty"`
	Negate    bool   `json:"negate,omitempty"`
}

ConditionRef references a registered condition. Negate inverts its result.

type Context

type Context struct {
	Ctx       context.Context
	DB        map[string]any
	Locale    string
	Separator string
	Flat      map[string]any
	RowID     string
	Field     *FieldView
	// contains filtered or unexported fields
}

Context is handed to every validator, operator, and condition. It carries the shared DB, the active locale and separator, the full flattened document, and metadata about the field currently being processed. Ctx is a standard context.Context so long-running custom rules can honor cancellation.

func (*Context) FieldPresent

func (c *Context) FieldPresent(target string) bool

FieldPresent reports whether target selects at least one value in the document under validation. It honors the active separator and wildcard rules.

func (*Context) Lookup

func (c *Context) Lookup(target string) (any, bool)

Lookup returns the first value selected by target, if any.

type Field

type Field struct {
	// Target selects flattened keys: a literal path, a path with a "*"
	// wildcard, or (when TargetRegex is true) a regular expression.
	Target      string `json:"target"`
	TargetRegex bool   `json:"targetRegex,omitempty"`

	Name        string `json:"name,omitempty"`
	DisplayName string `json:"displayName,omitempty"`
	Type        string `json:"type,omitempty"`
	Description string `json:"description,omitempty"`

	// Required fails when the target selects no value.
	Required bool `json:"required,omitempty"`
	// DependsOn lists other targets that must be present for this field to be
	// validated.
	DependsOn []string `json:"dependsOn,omitempty"`
	// AddToDB copies the matched value into the shared DB before validation, so
	// other rules can reference it.
	AddToDB bool `json:"addToDB,omitempty"`

	Tags []string `json:"tags,omitempty"`

	// When lists conditions that must all hold for the field to be processed.
	When []ConditionRef `json:"when,omitempty"`
	// Validate is the ordered chain of validators. The first failing rule stops
	// evaluation of this field and produces one error.
	Validate []RuleRef `json:"validate,omitempty"`
	// Operate is the ordered chain of operators applied by Operate.
	Operate []OperatorRef `json:"operate,omitempty"`

	Meta map[string]any `json:"meta,omitempty"`
}

Field targets one or more flattened keys and describes how to validate and transform the matched values.

type FieldBuilder added in v1.0.2

type FieldBuilder struct {
	// contains filtered or unexported fields
}

FieldBuilder accumulates one field's settings, validators, operators, and conditions. Any terminator method (Field, Done, Schema, New) commits it.

func (*FieldBuilder) AddToDB added in v1.0.2

func (fb *FieldBuilder) AddToDB() *FieldBuilder

AddToDB copies the matched value into the shared DB before validation.

func (*FieldBuilder) After added in v1.0.2

func (fb *FieldBuilder) After(date string) *FieldBuilder

func (*FieldBuilder) AfterNow added in v1.0.2

func (fb *FieldBuilder) AfterNow() *FieldBuilder

func (*FieldBuilder) Alpha added in v1.0.2

func (fb *FieldBuilder) Alpha() *FieldBuilder

func (*FieldBuilder) Alphanumeric added in v1.0.2

func (fb *FieldBuilder) Alphanumeric() *FieldBuilder

func (*FieldBuilder) Before added in v1.0.2

func (fb *FieldBuilder) Before(date string) *FieldBuilder

func (*FieldBuilder) BeforeNow added in v1.0.2

func (fb *FieldBuilder) BeforeNow() *FieldBuilder

func (*FieldBuilder) Between added in v1.0.2

func (fb *FieldBuilder) Between(min, max float64) *FieldBuilder

func (*FieldBuilder) Capitalize added in v1.0.2

func (fb *FieldBuilder) Capitalize() *FieldBuilder

func (*FieldBuilder) Contains added in v1.0.2

func (fb *FieldBuilder) Contains(sub string) *FieldBuilder

func (*FieldBuilder) Default added in v1.0.2

func (fb *FieldBuilder) Default(v any) *FieldBuilder

func (*FieldBuilder) DependsOn added in v1.0.2

func (fb *FieldBuilder) DependsOn(targets ...string) *FieldBuilder

DependsOn adds required-present dependency targets.

func (*FieldBuilder) Description added in v1.0.2

func (fb *FieldBuilder) Description(d string) *FieldBuilder

Description sets the field description.

func (*FieldBuilder) DisplayName added in v1.0.2

func (fb *FieldBuilder) DisplayName(n string) *FieldBuilder

DisplayName sets the human-facing name.

func (*FieldBuilder) Done added in v1.0.2

func (fb *FieldBuilder) Done() *SchemaBuilder

Done commits this field and returns the schema builder.

func (*FieldBuilder) Email added in v1.0.2

func (fb *FieldBuilder) Email() *FieldBuilder

func (*FieldBuilder) EndsWith added in v1.0.2

func (fb *FieldBuilder) EndsWith(suffix string) *FieldBuilder

func (*FieldBuilder) Equals added in v1.0.2

func (fb *FieldBuilder) Equals(v string) *FieldBuilder

func (*FieldBuilder) ExactLength added in v1.0.2

func (fb *FieldBuilder) ExactLength(n int) *FieldBuilder

func (*FieldBuilder) Field added in v1.0.2

func (fb *FieldBuilder) Field(target string) *FieldBuilder

Field commits this field and starts another.

func (*FieldBuilder) HasDigit added in v1.0.2

func (fb *FieldBuilder) HasDigit() *FieldBuilder

func (*FieldBuilder) HasLower added in v1.0.2

func (fb *FieldBuilder) HasLower() *FieldBuilder

func (*FieldBuilder) HasUpper added in v1.0.2

func (fb *FieldBuilder) HasUpper() *FieldBuilder

func (*FieldBuilder) InOptions added in v1.0.2

func (fb *FieldBuilder) InOptions(opts ...string) *FieldBuilder

func (*FieldBuilder) IsArray added in v1.0.2

func (fb *FieldBuilder) IsArray() *FieldBuilder

func (*FieldBuilder) IsBase64 added in v1.0.2

func (fb *FieldBuilder) IsBase64() *FieldBuilder

func (*FieldBuilder) IsBoolean added in v1.0.2

func (fb *FieldBuilder) IsBoolean() *FieldBuilder

func (*FieldBuilder) IsCIDR added in v1.0.2

func (fb *FieldBuilder) IsCIDR() *FieldBuilder

func (*FieldBuilder) IsDate added in v1.0.2

func (fb *FieldBuilder) IsDate() *FieldBuilder

func (*FieldBuilder) IsFloat added in v1.0.2

func (fb *FieldBuilder) IsFloat() *FieldBuilder

func (*FieldBuilder) IsHTTPS added in v1.0.2

func (fb *FieldBuilder) IsHTTPS() *FieldBuilder

func (*FieldBuilder) IsHex added in v1.0.2

func (fb *FieldBuilder) IsHex() *FieldBuilder

func (*FieldBuilder) IsIP added in v1.0.2

func (fb *FieldBuilder) IsIP() *FieldBuilder

func (*FieldBuilder) IsInteger added in v1.0.2

func (fb *FieldBuilder) IsInteger() *FieldBuilder

func (*FieldBuilder) IsJSON added in v1.0.2

func (fb *FieldBuilder) IsJSON() *FieldBuilder

func (*FieldBuilder) IsNumber added in v1.0.2

func (fb *FieldBuilder) IsNumber() *FieldBuilder

func (*FieldBuilder) IsObject added in v1.0.2

func (fb *FieldBuilder) IsObject() *FieldBuilder

func (*FieldBuilder) IsString added in v1.0.2

func (fb *FieldBuilder) IsString() *FieldBuilder

func (*FieldBuilder) IsURL added in v1.0.2

func (fb *FieldBuilder) IsURL() *FieldBuilder

func (*FieldBuilder) IsUUID added in v1.0.2

func (fb *FieldBuilder) IsUUID() *FieldBuilder

func (*FieldBuilder) ItemsInOptions added in v1.0.2

func (fb *FieldBuilder) ItemsInOptions(opts ...string) *FieldBuilder

func (*FieldBuilder) LengthBetween added in v1.0.2

func (fb *FieldBuilder) LengthBetween(min, max int) *FieldBuilder

func (*FieldBuilder) Like added in v1.0.2

func (fb *FieldBuilder) Like(p string) *FieldBuilder

func (*FieldBuilder) Lower added in v1.0.2

func (fb *FieldBuilder) Lower() *FieldBuilder

func (*FieldBuilder) Max added in v1.0.2

func (fb *FieldBuilder) Max(v float64) *FieldBuilder

func (*FieldBuilder) MaxItems added in v1.0.2

func (fb *FieldBuilder) MaxItems(n int) *FieldBuilder

func (*FieldBuilder) MaxLength added in v1.0.2

func (fb *FieldBuilder) MaxLength(n int) *FieldBuilder

func (*FieldBuilder) Message added in v1.0.2

func (fb *FieldBuilder) Message(msg string) *FieldBuilder

Message sets the custom message on the most recently added validator.

func (*FieldBuilder) Meta added in v1.0.2

func (fb *FieldBuilder) Meta(m map[string]any) *FieldBuilder

Meta attaches arbitrary metadata.

func (*FieldBuilder) Min added in v1.0.2

func (fb *FieldBuilder) Min(v float64) *FieldBuilder

func (*FieldBuilder) MinItems added in v1.0.2

func (fb *FieldBuilder) MinItems(n int) *FieldBuilder

func (*FieldBuilder) MinLength added in v1.0.2

func (fb *FieldBuilder) MinLength(n int) *FieldBuilder

func (*FieldBuilder) MultipleOf added in v1.0.2

func (fb *FieldBuilder) MultipleOf(of float64) *FieldBuilder

func (*FieldBuilder) Name added in v1.0.2

func (fb *FieldBuilder) Name(n string) *FieldBuilder

Name sets the machine name.

func (*FieldBuilder) Negative added in v1.0.2

func (fb *FieldBuilder) Negative() *FieldBuilder

func (*FieldBuilder) New added in v1.0.2

func (fb *FieldBuilder) New(opts ...Option) (*Schematics, error)

New commits this field and builds a checked *Schematics.

func (*FieldBuilder) NonNegative added in v1.0.2

func (fb *FieldBuilder) NonNegative() *FieldBuilder

func (*FieldBuilder) NotEmpty added in v1.0.2

func (fb *FieldBuilder) NotEmpty() *FieldBuilder

func (*FieldBuilder) NotInOptions added in v1.0.2

func (fb *FieldBuilder) NotInOptions(opts ...string) *FieldBuilder

func (*FieldBuilder) Op added in v1.0.2

func (fb *FieldBuilder) Op(name string, args Args) *FieldBuilder

Op appends an arbitrary operator by name.

func (*FieldBuilder) Pattern added in v1.0.2

func (fb *FieldBuilder) Pattern(p string) *FieldBuilder

func (*FieldBuilder) Positive added in v1.0.2

func (fb *FieldBuilder) Positive() *FieldBuilder

func (*FieldBuilder) Regex added in v1.0.2

func (fb *FieldBuilder) Regex() *FieldBuilder

Regex marks the target as a regular expression.

func (*FieldBuilder) Replace added in v1.0.2

func (fb *FieldBuilder) Replace(old, new string) *FieldBuilder

func (*FieldBuilder) Required added in v1.0.2

func (fb *FieldBuilder) Required() *FieldBuilder

Required marks the field required.

func (*FieldBuilder) Rule added in v1.0.2

func (fb *FieldBuilder) Rule(name string, args Args) *FieldBuilder

Rule appends an arbitrary validator by name.

func (*FieldBuilder) Schema added in v1.0.2

func (fb *FieldBuilder) Schema() Schema

Schema commits this field and returns the assembled Schema.

func (*FieldBuilder) Slugify added in v1.0.2

func (fb *FieldBuilder) Slugify() *FieldBuilder

func (*FieldBuilder) StartsWith added in v1.0.2

func (fb *FieldBuilder) StartsWith(prefix string) *FieldBuilder

func (*FieldBuilder) Tags added in v1.0.2

func (fb *FieldBuilder) Tags(tags ...string) *FieldBuilder

Tags sets the field tags.

func (*FieldBuilder) ToString added in v1.0.2

func (fb *FieldBuilder) ToString() *FieldBuilder

func (*FieldBuilder) Trim added in v1.0.2

func (fb *FieldBuilder) Trim() *FieldBuilder

func (*FieldBuilder) Truncate added in v1.0.2

func (fb *FieldBuilder) Truncate(length int, suffix string) *FieldBuilder

func (*FieldBuilder) Type added in v1.0.2

func (fb *FieldBuilder) Type(t string) *FieldBuilder

Type sets the declared type (enforced when the engine has WithTypeChecks).

func (*FieldBuilder) Unique added in v1.0.2

func (fb *FieldBuilder) Unique() *FieldBuilder

func (*FieldBuilder) Upper added in v1.0.2

func (fb *FieldBuilder) Upper() *FieldBuilder

func (*FieldBuilder) When added in v1.0.2

func (fb *FieldBuilder) When(condition string, args Args) *FieldBuilder

When appends a condition that must hold for the field to run.

func (*FieldBuilder) WhenAbsent added in v1.0.2

func (fb *FieldBuilder) WhenAbsent(field string) *FieldBuilder

func (*FieldBuilder) WhenEquals added in v1.0.2

func (fb *FieldBuilder) WhenEquals(field string, value any) *FieldBuilder

func (*FieldBuilder) WhenNot added in v1.0.2

func (fb *FieldBuilder) WhenNot(condition string, args Args) *FieldBuilder

WhenNot appends a negated condition.

func (*FieldBuilder) WhenPresent added in v1.0.2

func (fb *FieldBuilder) WhenPresent(field string) *FieldBuilder

type FieldView

type FieldView struct {
	Target   string
	Name     string
	Type     string
	Required bool
	Provided bool
	Tags     []string
}

FieldView is a read-only snapshot of the field a rule is running against.

type Operator

type Operator func(value any, args Args, ctx *Context) (any, error)

Operator transforms a value and returns the replacement. Returning an error aborts the whole Operate call.

type OperatorRef

type OperatorRef struct {
	Op   string `json:"op"`
	Args Args   `json:"args,omitempty"`
}

OperatorRef references a registered operator and its arguments.

type Option

type Option func(*Schematics)

Option configures a Schematics at construction time.

func WithArrayIDKey

func WithArrayIDKey(key string) Option

WithArrayIDKey sets the flattened key whose value identifies each row when validating an array of objects.

func WithCollectAll added in v1.0.2

func WithCollectAll() Option

WithCollectAll makes Validate report every failing rule on each field rather than stopping at the first failure. Useful for form validation where the caller wants to show all problems at once.

func WithDB

func WithDB(db map[string]any) Option

WithDB seeds the shared DB that is passed to every rule.

func WithLocale

func WithLocale(locale string) Option

WithLocale sets the default locale for error messages.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger attaches a slog.Logger. By default logs are discarded.

func WithMaxBodyBytes added in v1.0.2

func WithMaxBodyBytes(n int64) Option

WithMaxBodyBytes caps how many bytes the API layer reads from a request body, guarding against unbounded-read denial of service. A value <= 0 means unlimited. It has no effect on the plain Validate path.

func WithSeparator

func WithSeparator(sep string) Option

WithSeparator sets the key separator used when flattening documents.

func WithTypeChecks added in v1.0.2

func WithTypeChecks() Option

WithTypeChecks makes a field's "type" enforce a matching built-in check (string, number, integer, boolean, array, date, object) before its own validators run. Without this option, "type" is treated as documentation only, preserving the default behavior.

type Rule

type Rule func(value any, args Args, ctx *Context) error

Rule validates a value. It returns nil when the value is acceptable, or an error describing why it is not. Rules must never panic: use the typed Args accessors and the value-inspection helpers, both of which report bad input as errors.

type RuleRef

type RuleRef struct {
	Rule     string            `json:"rule"`
	Args     Args              `json:"args,omitempty"`
	Message  string            `json:"message,omitempty"`
	Messages map[string]string `json:"messages,omitempty"`
}

RuleRef references a registered validator and its per-use configuration.

type Schema

type Schema struct {
	Version    string         `json:"version,omitempty"`
	Separator  string         `json:"separator,omitempty"`
	ArrayIDKey string         `json:"arrayIdKey,omitempty"`
	Locale     string         `json:"locale,omitempty"`
	DB         map[string]any `json:"db,omitempty"`
	Fields     []Field        `json:"fields"`
}

Schema is the declarative document that drives validation and operation.

Example (JSON):

{
  "version": "2.0",
  "separator": ".",
  "arrayIdKey": "id",
  "locale": "en",
  "db": { "minAge": 18 },
  "fields": [
    {
      "target": "user.profile.name",
      "type": "string",
      "required": true,
      "validate": [ { "rule": "minLength", "args": { "min": 2 } } ],
      "operate":  [ { "op": "trim" }, { "op": "capitalize" } ]
    }
  ]
}

type SchemaBuilder added in v1.0.2

type SchemaBuilder struct {
	// contains filtered or unexported fields
}

SchemaBuilder accumulates schema-level settings and fields.

func NewSchema added in v1.0.2

func NewSchema() *SchemaBuilder

NewSchema starts a fluent schema definition.

func (*SchemaBuilder) ArrayIDKey added in v1.0.2

func (b *SchemaBuilder) ArrayIDKey(k string) *SchemaBuilder

ArrayIDKey sets the per-row identifier key for array validation.

func (*SchemaBuilder) DB added in v1.0.2

func (b *SchemaBuilder) DB(m map[string]any) *SchemaBuilder

DB seeds the shared DB baked into the schema.

func (*SchemaBuilder) Field added in v1.0.2

func (b *SchemaBuilder) Field(target string) *FieldBuilder

Field begins a new field targeting target.

func (*SchemaBuilder) Locale added in v1.0.2

func (b *SchemaBuilder) Locale(l string) *SchemaBuilder

Locale sets the default error locale.

func (*SchemaBuilder) New added in v1.0.2

func (b *SchemaBuilder) New(opts ...Option) (*Schematics, error)

New builds a *Schematics from the schema, applies opts, and runs Check so a malformed schema fails fast.

func (*SchemaBuilder) Schema added in v1.0.2

func (b *SchemaBuilder) Schema() Schema

Schema returns the assembled Schema value.

func (*SchemaBuilder) Separator added in v1.0.2

func (b *SchemaBuilder) Separator(sep string) *SchemaBuilder

Separator sets the flattening separator.

type SchemaError

type SchemaError struct {
	Problems []string
}

SchemaError is returned when a schema references a rule, operator, or condition that is not registered, or is otherwise malformed. It is distinct from ValidationErrors, which reports problems with the data.

func (*SchemaError) Error

func (e *SchemaError) Error() string

type Schematics

type Schematics struct {
	// contains filtered or unexported fields
}

Schematics is the entry point: it holds a schema, the registries of validators/operators/conditions (pre-loaded with the built-ins), and configuration. Create one with New, load a schema, then call Validate or Operate.

A Schematics is safe for concurrent use by multiple goroutines once its schema is loaded and any custom rules are registered: Validate, ValidateCtx, Operate, ValidateBytes and the API layer may all be called in parallel on a shared instance. Registering rules or loading a new schema is not safe to do concurrently with validation — do that during setup, before sharing.

func ImportSchema added in v1.0.2

func ImportSchema(b []byte, opts ...Option) (*Schematics, error)

ImportSchema is a convenience constructor for callers who start from raw schema bytes (a config file already read into memory, a network response, an embedded fixture): it combines New and LoadBytes into one call.

s, err := schematics.ImportSchema(schemaBytes)
if err != nil {
    log.Fatal(err)
}

Any options are applied the same way they are for New, before the schema is loaded.

func New

func New(opts ...Option) *Schematics

New creates a Schematics with every built-in validator, operator, and condition registered, then applies the given options.

func (*Schematics) Check

func (s *Schematics) Check() error

Check verifies that every rule, operator, and condition referenced by the schema is registered and that fields are well formed. It returns a *SchemaError describing all problems, or nil.

func (*Schematics) ConditionNames

func (s *Schematics) ConditionNames() []string

ConditionNames returns the names of every registered condition.

func (*Schematics) LoadBytes

func (s *Schematics) LoadBytes(b []byte) error

LoadBytes parses a JSON schema from b.

func (*Schematics) LoadFile

func (s *Schematics) LoadFile(path string) error

LoadFile reads and parses a JSON schema file.

func (*Schematics) LoadMap

func (s *Schematics) LoadMap(m any) error

LoadMap parses a schema from an in-memory value (a map or struct) by round-tripping it through JSON.

func (*Schematics) Operate

func (s *Schematics) Operate(data any) (any, error)

Operate applies each field's operator chain to the matching values in data and returns the transformed document. data may be a single object or an array of objects. Validators are not run.

func (*Schematics) OperateCtx

func (s *Schematics) OperateCtx(ctx context.Context, data any) (any, error)

OperateCtx is Operate with a caller-supplied context.Context.

func (*Schematics) OperatorNames

func (s *Schematics) OperatorNames() []string

OperatorNames returns the names of every registered operator.

func (*Schematics) RegisterCondition

func (s *Schematics) RegisterCondition(name string, fn Condition) *Schematics

RegisterCondition adds or replaces a named condition.

func (*Schematics) RegisterOperator

func (s *Schematics) RegisterOperator(name string, fn Operator) *Schematics

RegisterOperator adds or replaces a named operator.

func (*Schematics) RegisterRule

func (s *Schematics) RegisterRule(name string, fn Rule) *Schematics

RegisterRule adds or replaces a named validator. It is safe to call after loading a schema and before Validate.

func (*Schematics) RuleNames

func (s *Schematics) RuleNames() []string

RuleNames returns the names of every registered validator.

func (*Schematics) Schema

func (s *Schematics) Schema() Schema

Schema returns the currently loaded schema.

func (*Schematics) SetSchema

func (s *Schematics) SetSchema(schema Schema) *Schematics

SetSchema installs a schema value directly.

func (*Schematics) Validate

func (s *Schematics) Validate(data any) error

Validate checks data against the loaded schema. data may be a single object (map or struct) or an array of objects. It returns nil when the data is valid, a *ValidationErrors when it is not, or a *SchemaError when the schema itself references something unregistered.

func (*Schematics) ValidateBytes added in v1.0.2

func (s *Schematics) ValidateBytes(b []byte, isArray bool) error

ValidateBytes parses raw JSON data bytes and validates them against the loaded schema, saving the caller a manual json.Unmarshal before calling Validate. isArray tells it whether to parse b as a single JSON object (false) or as an array of objects (true) — pass it explicitly rather than relying on shape-sniffing, so a payload that doesn't match the expected shape fails with a clear parse error instead of silently going through Validate's try-object-then-array fallback.

func (*Schematics) ValidateBytesCtx added in v1.0.2

func (s *Schematics) ValidateBytesCtx(ctx context.Context, b []byte, isArray bool) error

ValidateBytesCtx is ValidateBytes with a caller-supplied context.Context, made available to every rule via Context.Ctx.

func (*Schematics) ValidateCtx

func (s *Schematics) ValidateCtx(ctx context.Context, data any) error

ValidateCtx is Validate with a caller-supplied context.Context, made available to every rule via Context.Ctx.

func (*Schematics) ValidateSchema added in v1.0.1

func (s *Schematics) ValidateSchema(sample any, ignoreTargets ...string) error

ValidateSchema runs every check that Check does — unknown rule/operator/ condition names, empty targets, duplicate targets — and additionally verifies that every field's target actually resolves against sample once flattened. sample should be a representative example of the data you intend to validate: a test fixture, a golden payload, or real (secret- stripped) production data.

Check alone cannot catch a typo inside a target's value. Given `"target": "mane"` instead of `"name"`, "mane" is a perfectly valid string, so Check has nothing to object to — the field just silently never matches anything at runtime. ValidateSchema closes that gap by matching every target against real data and flagging the ones that match nothing.

A typo in the JSON *key* itself — `"tagret"` instead of `"target"` — never needs sample data to catch: encoding/json silently drops an unrecognized key, so Target is left as "" and Check already reports "field #N has an empty target". ValidateSchema surfaces that too, since it runs Check first.

Pass ignoreTargets for fields that are legitimately allowed to be missing from every sample you have — an optional field with no example value in your fixtures, say — so they are not flagged as suspected typos.

Wildcard (target containing "*") and targetRegex fields are matched the same way they are at validation time: if nothing in sample matches the pattern, the field is flagged. If that's simply because your sample doesn't happen to contain that shape, add the target to ignoreTargets rather than treating the result as a bug in the schema.

ValidateSchema does not replace tests. It is meant to run in CI or a unit test alongside a fixture, so drift between your schema and your real data shape fails the build instead of failing silently in production.

type ValidationError

type ValidationError struct {
	// Target is the concrete flattened key that failed, e.g. "user.profile.name".
	Target string
	// Rule is the name of the validator (or "required"/"dependsOn") that failed.
	Rule string
	// Value is the offending value.
	Value any
	// RowID identifies the array row for array inputs; empty for plain objects.
	RowID string
	// contains filtered or unexported fields
}

ValidationError describes a single rule that failed on a single target value.

func (*ValidationError) Error

func (e *ValidationError) Error() string

Error implements the error interface using the default locale.

func (*ValidationError) Format

func (e *ValidationError) Format(locale, format string) string

Format renders the error using a template. Recognized tokens are %message, %target, %rule (alias %validator), %value and %id.

func (*ValidationError) MarshalJSON

func (e *ValidationError) MarshalJSON() ([]byte, error)

MarshalJSON renders the error as a stable JSON object.

func (*ValidationError) Message

func (e *ValidationError) Message(locale string) string

Message returns the message for locale, falling back to the default message and then to a generated description.

type ValidationErrors

type ValidationErrors struct {
	Errors []*ValidationError
}

ValidationErrors is the aggregate error returned by Validate. It implements the error interface, so callers can use errors.As to recover it.

func (*ValidationErrors) Add

func (es *ValidationErrors) Add(e *ValidationError)

Add appends a non-nil error.

func (*ValidationErrors) Error

func (es *ValidationErrors) Error() string

Error implements the error interface.

func (*ValidationErrors) ForTarget

func (es *ValidationErrors) ForTarget(target string) []*ValidationError

ForTarget returns the subset of errors whose Target equals target.

func (*ValidationErrors) HasErrors

func (es *ValidationErrors) HasErrors() bool

HasErrors reports whether any errors were collected.

func (*ValidationErrors) Len

func (es *ValidationErrors) Len() int

Len returns the number of collected errors.

func (*ValidationErrors) Messages

func (es *ValidationErrors) Messages(locale string) []string

Messages returns just the localized messages.

func (*ValidationErrors) Strings

func (es *ValidationErrors) Strings(locale, format string) []string

Strings renders every error with the given locale and format template.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL