How to use Shape in Go#
Goal: apply everything in these docs from Go, accounting for the language differences from the canonical TypeScript.
Install and import#
go get github.com/rjrodger/shape/go
import "github.com/rjrodger/shape/go" // package name is `shape`
Requires Go 1.22+.
Sentinel tokens instead of constructors#
Go cannot use predeclared types (string, int) as runtime map values, so the
package exports sentinel tokens:
| Token | Matches |
|---|---|
shape.Any |
any value |
shape.String |
strings |
shape.Number |
any numeric kind (int*, uint*, float*) |
shape.Boolean |
booleans |
shape.Object |
map[string]any (or a struct, see Structs) |
shape.Array |
[]any |
shape.Function |
any reflect.Func value |
shape.Integer |
a number with no fractional part |
shape.Date |
time.Time values |
For a dot-import style without clashing with stdlib names, G-prefixed aliases
exist: GString, GNumber, GMin, GRequired, …
Compile and validate#
s, err := shape.Shape(spec) // (*Schema, error)
s := shape.MustShape(spec) // panics on a bad spec
s := shape.MustShapeWith(spec, opts)
out, err := s.Validate(input) // produced value + *ValidationError
out, err := s.ValidateCtx(input, ctx) // with a *shape.Context
ok := s.Match(input) // bool, no errors built
ok := s.Valid(input) // alias of Match
issues := s.Error(input) // []FieldError, nil when valid
std := s.Standard() // Standard Schema V1-style interface
Values are JSON-shaped#
Validate map[string]any, []any, string, bool and numeric
values—typically the result of json.Unmarshal. Numbers compare as float64. Typed
slices are accepted and coerced to []any.
Structs#
A struct (or pointer to one) is accepted wherever an object is, read by its
json tags as encoding/json would encode it: json:"-" hides a field,
omitempty makes a zero value absent, embedded structs are promoted. The
produced value is still a map; ValidateInto decodes it back into a struct.
type User struct {
Name string `json:"name"`
Age int `json:"age,omitempty"`
}
s := shape.MustShape(map[string]any{"name": shape.String, "age": 42})
var u User
if err := s.ValidateInto(User{Name: "Ann"}, &u); err != nil { ... }
// u == User{Name: "Ann", Age: 42}
A struct can also be the spec: each field's value is its default and a
shape tag holds a key expression, so Port int tagged shape:"Min(1).Max(65535)"
means what the map key "Port: Min(1).Max(65535)" means. See
Structs in the API reference.
Numbers#
shape.Number accepts every numeric kind. There is no single "number" type in
Go, so a JSON number arrives as float64; native ints/uints/floats are also
accepted. Type checks, bounds (Min, Max, …) and Exact compare numbers
by value whatever their kind, so Exact(1) matches the float64 1.0 that
json.Unmarshal produces. Anything else Exact compares with
reflect.DeepEqual, so a string, slice or map literal must have the Go type
the input will have.
undefined vs null#
Go has no undefined. A missing map key is treated as absent (may be
defaulted or required); an explicit nil value is treated as a present null
(a type error against a typed shape), mirroring the TypeScript distinction.
Validate(nil) at the top level means "no value supplied" and fills defaults.
Positional argument validation#
MakeArgu builds a validator for a function's positional arguments:
argu := shape.MakeArgu("connect")
args, err := argu.Validate(
[]any{"localhost", 8080.0},
"host, port",
map[string]any{"a": shape.String, "b": shape.Number},
)
// args == map[string]any{"a": "localhost", "b": 8080}
Because Go maps are unordered, argument specs are ordered alphabetically by
key—use a, b, c, … to fix positions. argu.Partial(...) returns a
reusable closure.
Differences to keep in mind#
The full list is in TypeScript ↔ Go parity. The headline item is inherent to Go: alphabetical key ordering (Go maps are unordered). Regular expressions are held to the shared regexp subset, so a pattern reads the same here as in TypeScript and Rust.