shape

Go API reference#

Module github.com/rjrodger/shape/go, package shape. Requires Go 1.22+.

Compilation#

func Shape(spec any) (*Schema, error)
func ShapeWith(spec any, opts ShapeOptions) (*Schema, error)
func MustShape(spec any) *Schema
func MustShapeWith(spec any, opts ShapeOptions) *Schema
func IsShape(v any) bool

*Schema methods#

func (s *Schema) Validate(input any) (any, error) // *ValidationError
func (s *Schema) ValidateCtx(input any, ctx *Context) (any, error)
func (s *Schema) Match(input any) bool
func (s *Schema) Valid(input any) bool                  // alias of Match
func (s *Schema) Error(input any) []FieldError          // nil when valid
func (s *Schema) Spec() any                             // JSON-friendly
func (s *Schema) Node() *node                           // introspection
func (s *Schema) String() string                        // debug render
func (s *Schema) JSON() (any, error)                    // declarative JSON, read back by Build
func (s *Schema) JSONSchema() map[string]any            // JSON Schema, draft 2020-12
func (s *Schema) ValidateInto(input any, out any) error // fill a struct with the result
func (s *Schema) Standard() StandardSchema              // Standard Schema V1-style interface

(*Node).JSONSchema() renders a built node the same way; (*Node).Kind() is its kind, (*Node).Meta() its metadata (sidecar keys and Describe's description) and (*Node).Inner() the underlying private node, as (*Schema).Node() is for the root.

func FromJSONSchema(schema any) (any, error) // a spec built from a JSON Schema (as decoded by encoding/json)
func MustFromJSONSchema(schema any) any

See Export and import a JSON Schema.

Structs#

The validator works on JSON-shaped values (map[string]any, []any, strings, numbers, booleans); structs are read into that model on the way in and filled from the produced value on the way out. A struct and the map it reads as validate identically, so nothing here affects parity with TypeScript.

As a value. Any struct, or pointer to one, is accepted where an object is. A field is named by its json tag or its own name; json:"-" hides it; json:",omitempty" makes a zero value absent (so a default fills it, or a required check fires) instead of present; an embedded struct's fields are promoted, as encoding/json promotes them; unexported fields are not read. Nested structs, pointers (a nil pointer is a present null), slices, arrays and string-keyed maps of any value type are converted recursively; time.Time is kept as a date. ValidateInto validates and then decodes the produced value into out (a pointer to a struct, or anything encoding/json can decode into) in one step.

type User struct {
	Name string `json:"name"`
	Age  int    `json:"age,omitempty"`
}
s := shape.MustShape(map[string]any{"name": shape.String, "age": 42})

out, err := s.Validate(User{Name: "Ann"}) // map[string]any{"name":"Ann","age":42}
var u User
err = s.ValidateInto(map[string]any{"name": "Ann"}, &u) // u.Age == 42

As a spec. A struct value is also a spec by example, as a map is: each field's value is its default, and a shape tag holds a key expression applied to it, exactly as "Port: Min(1).Max(65535)" would in a map key. Field names follow the same json tag rules (omitempty is ignored, since a spec field is a default whether or not it is zero).

type Config struct {
	Host  string `shape:"Min(1)"`            // non-empty string, default ""
	Port  int    `shape:"Min(1).Max(65535)"` // default 0 — so set one below
	Debug bool   `shape:"Boolean"`           // required
	Name  string `json:"name" shape:"String"`
}
s := shape.MustShape(Config{Host: "localhost", Port: 8080})
// reads as {"Host: Min(1)": "localhost", "Port: Min(1).Max(65535)": 8080,
//           "Debug: Boolean": false, "name: String": ""}

Tokens#

Sentinel TypeToken values used in a spec to require a type:

var Any, String, Number, Boolean, Object, Array, Function, Integer, Date TypeToken

func (t TypeToken) Kind() Kind

Any is the one token that does not require a value, so { "a": Any } accepts an object without a. To narrow it, use Type(Any, spec). Integer is a number with no fractional part; Date is a time.Time, and a time.Time in a spec is an optional date with that default. Integer and Date are builder functions in TypeScript, so the chain shortcuts .Integer() and .Date() are how a node is narrowed to them here.

Kind is the normalized kind identifier (KindString, KindNumber, KindBoolean, KindObject, KindArray, KindAny, KindNull, KindNaN, KindFunction, KindNever, KindCheck, KindRegexp, KindInteger, KindDate, KindList).

Builders#

Every builder is a top-level function returning *Node; most also have a chainable *Node method. See the builder reference for what each does; the Go signatures are:

func Required(spec ...any) *Node
func Optional(spec ...any) *Node
func Default(dval any, spec ...any) *Node
func Skip(spec ...any) *Node
func Ignore(spec ...any) *Node
func Empty(spec ...any) *Node
func Nullable(spec ...any) *Node
func Fault(msg string, spec ...any) *Node
func Type(kind any, spec ...any) *Node // Kind, TypeToken, kind name or *Node
func Exact(vals ...any) *Node          // numbers by value (Exact(1) matches 1.0), the rest by reflect.DeepEqual
func Never(spec ...any) *Node
func Func(spec ...any) *Node
func Coerce(spec ...any) *Node
func Email(spec ...any) *Node
func Url(spec ...any) *Node
func Uuid(spec ...any) *Node
func DateTime(spec ...any) *Node
func Ip(spec ...any) *Node
func Ipv4(spec ...any) *Node
func Ipv6(spec ...any) *Node
func Min(min any, spec ...any) *Node
func Max(max any, spec ...any) *Node
func Above(above any, spec ...any) *Node
func Below(below any, spec ...any) *Node
func Len(length int, spec ...any) *Node
func One(shapes ...any) *Node
func Some(shapes ...any) *Node
func All(shapes ...any) *Node
func Open(spec ...any) *Node
func Closed(spec ...any) *Node
func Child(child any, spec ...any) *Node
func Rest(child any, spec ...any) *Node
func Define(name string, spec ...any) *Node
func Key(args ...any) *Node // Key(), Key(depth) or Key(depth, sep)

Options carriers:

type ReferOptions struct {
	Fill   bool // substitute even when the value is absent
	Strict bool // a name with no Define is an error, not a no-op
}
type RenameOptions struct {
	Keep  bool     // keep the original key too
	Claim []string // alternative source keys to read from
}

func Refer(name string, spec ...any) *Node
func ReferWith(name string, opts ReferOptions, spec ...any) *Node
func Rename(name string, spec ...any) *Node
func RenameWith(name string, opts RenameOptions, spec ...any) *Node

Builders that take more than a shape:

func Discriminated(tag string, branches map[string]any) *Node // top-level only
func Catch(fallback any, spec ...any) *Node
func Transform(fn func(val any, state *State) any, spec ...any) *Node
func Describe(description string, spec ...any) *Node // read back with (*Node).Meta()
func Pick(names any, spec ...any) *Node              // names: string, []string or []any
func Omit(names any, spec ...any) *Node
func Partial(spec ...any) *Node
func Extend(extra any, spec ...any) *Node

A builder called wrongly—Discriminated without a branch, Pick of an unknown property—cannot return an error, so the fault surfaces at validation as a never node carrying the message, as for any bad spec. In the string DSL Expr returns it as an error.

G-prefixed aliases exist for every builder and token (GString, GMin, GRequired, …) for use with a dot-import.

Chainable methods#

Every builder that takes only a shape is also a *Node method, so specs read as a chain: Optional().Number().Min(2).

Above    After    Any      Before   Below    Catch    Check    Child
Closed   Coerce   DateTime Default  Define   Describe Email    Empty
Exact    Extend   Fault    Func     Ignore   Ip       Ipv4     Ipv6
Len      Max      Min      Never    Nullable Omit     Open     Optional
Partial  Pick     Refer    Rename   Required Rest     Skip     Transform
Type     Url      Uuid

plus the type shortcuts .Number(), .Boolean(), .Object(), .Array(), .Function(), .Integer() and .Date(). There is no .String(): a method of that name on an exported type reads as fmt.Stringer and go vet rejects the signature—use .Type(String), which is what the shortcuts call anyway.

The object algebra methods (.Pick, .Omit, .Partial, .Extend) return a new node and leave the receiver as it was; every other chain method narrows the receiver in place and returns it.

Absent versus null#

var Null any

Go cannot tell a missing argument from a nil one, so Validate(nil) means "no value supplied" (JS undefined) and defaults fill, mirroring TS Shape(x)(). Pass Null to mean a value that is present and null (JS null), which is a type error against a typed shape:

s := shape.MustShape(1.0)
s.Validate(nil)        // 1, nil — absent, so the default fills
s.Validate(shape.Null) // nil, "Validation failed for value \"null\" because the value is not of type number."

Inside a map or slice a plain nil already reads as present-null, because the key or index exists; Null is accepted there too and means the same thing.

Options#

type ShapeOptions struct {
	KeyExpr KeyExprOptions // "x: Min(1)" key parsing
	Meta    MetaOptions    // "x$$" sidecar metadata
	ValExpr ValExprOptions // "$$" value expressions
}
type KeyExprOptions struct{ Disable bool } // default: enabled
type MetaOptions struct {                  // default: off, "$$"
	Active bool
	Suffix string
}
type ValExprOptions struct { // default: off, "$$"
	Active  bool
	KeyMark string
}

Errors#

type FieldError struct {
	Path    string // dot-notation path, e.g. "users.0.email"
	PathArr []any  // the path as an array: indices as ints, keys as strings
	Key     string
	Type    Kind
	Value   any
	Why     string
	Mark    int
	Text    string
	Check   string
	Args    map[string]any
}

func (e FieldError) Error() string

type ValidationError struct{ Issues []FieldError }

func (e *ValidationError) Error() string // the issues' Text, joined by newline

Why codes are exported as constants: WhyType, WhyRequired, WhyClosed, WhyCheck, WhyNever, WhyRegexp, WhyEmpty (the lower-case codes, equal to the TypeScript why strings) and one per builder that fails on its own (WhyMin, WhyExact, WhyEmail, WhyDiscriminated, …). The builder codes are a deliberate divergence: where Go reports WhyMin with mark 4011, TypeScript reports why: "check" with the builder name in check and mark 4000, so code that branches on a why code must not expect the same string in both (see the parity page).

Standard Schema#

A Go rendering of the TypeScript ~standard object: a version, a vendor and a Validate that never panics and returns either the produced value or a list of issues, never both.

type StandardSchema struct {
	Version  int                            // always 1
	Vendor   string                         // always "shape"
	Validate func(input any) StandardResult // non-throwing validation
}
type StandardResult struct {
	Value  any             // produced value when Issues is empty
	Issues []StandardIssue // empty on success
}
type StandardIssue struct {
	Message string // FieldError.Text
	Path    []any  // FieldError.PathArr
}

func (s *Schema) Standard() StandardSchema

Custom validators#

type State struct {
	Path    []string // path stack from root; current key at end
	PathArr []any    // path as array: array indices as ints, object keys as strings
	Key     string   // immediate key/index name
	Value   any      // current value being validated
	Node    *node    // current node
	Parent  any      // parent map/slice
	Match   bool     // true when invoked via Match (no mutation, no error report)
	Ctx     *Context // user/custom context
}
type Update struct {
	Done    bool   // stop running further checks
	Why     string // why code on failure
	Mark    int    // numeric mark on failure
	Err     any    // string, FieldError, or []FieldError
	Val     any    // replacement value
	HasVal  bool   // true if Val should override
	Node    *node  // override node (used by Refer)
	Replace bool   // compat marker, not currently consulted
}
type Context struct {
	Err    []FieldError
	Custom map[string]any   // cross-property state for custom validators
	Refs   map[string]*node // used by Define/Refer
	Match  bool
}

func Before(fn func(val any, u *Update, s *State) bool, spec ...any) *Node
func After(fn func(val any, u *Update, s *State) bool, spec ...any) *Node
func Check(check any, spec ...any) *Node // func(...) bool or *regexp.Regexp

ValidateCtx runs a validation with a *Context the caller supplies, so custom validators can share state through Custom.

String DSL#

func Expr(src string) (*Node, error)
func MustExpr(src string) *Node
func Build(spec any) (*Schema, error)

Positional arguments (Argu)#

func MakeArgu(name string) Argu
func (a Argu) Validate(args []any, whence string, spec map[string]any) (map[string]any, error)
func (a Argu) Partial(whence string, spec map[string]any) func([]any) (map[string]any, error)

Argument specs are ordered alphabetically by key (Go maps are unordered), so name keys a, b, c, … to fix argument positions.

Version#

const Version = "0.5.0"

See Use Shape in Go for idioms and the parity notes for behavioural differences.