shape

How to add custom validation#

Goal: enforce a rule that the built-in builders don't cover—a regular expression, or arbitrary logic.

Regular expressions#

Pass a RegExp (TS) or *regexp.Regexp (Go) to Check.

TS

const { Shape, Check } = require('shape')
const shape = Shape({ email: Check(/^.+@.+$/) })

shape({ email: 'a@b.co' })   // OK
shape({ email: 'nope' })     // throws: check "/^.+@.+$/" failed

Go

import "regexp"

s := shape.MustShape(map[string]any{
    "email": shape.Check(regexp.MustCompile(`^.+@.+$`)),
})

A pattern is held to the shared regexp subset: what the JavaScript, RE2 and regex-crate engines all read the same way, with \d, \w and \s ASCII and no flags, lookaround or backreferences. Inside it a shape behaves identically in every language.

A custom predicate#

Pass a function to Check. Return true to pass. You may also read/replace the value and set a custom error.

TS

const shape = Shape({
  even: Check((val, update, state) => {
    if (typeof val !== 'number' || val % 2 !== 0) {
      update.err = 'must be an even number'
      return false
    }
    return true
  }),
})

Go

s := shape.MustShape(map[string]any{
    "even": shape.Check(func(val any, u *shape.Update, s *shape.State) bool {
        n, ok := val.(float64)
        if !ok || int(n)%2 != 0 {
            u.Err = "must be an even number"
            return false
        }
        return true
    }),
})

The validator signature is the same in both languages: (val, update, state) => bool. See Shape nodes for the update/state fields.

Before and after the structural checks#

Shape({
  id: Before((val, update) => (undefined !== val && (update.val = String(val)), true), String),
})

A Before also runs when the property is absent (val is undefined), so leave an absent value alone or the required check never runs—without the preceding guard, a missing id would become the string 'undefined'.

Coerce, repair or rewrite the value#

For the common cases there is no need to write a validator:

Replace the message#

Wrap any shape in Fault to override the structural error text (custom-check messages are controlled by the check itself):

Shape({ port: Fault('port must be a number', Number) })

Do not use an async function#

Validation is synchronous by design (see ADR 0001). An async validator returns a promise, a promise is truthy, and a truthy return means "passed"—so the check silently succeeds no matter what it would have decided:

const check = async (v) => v > 100     // would reject 5
Shape(Check(check, Number))(5)         // → 5. Passes.

Validate the shape first, then apply rules that need I/O to the produced value:

const user = Shape({ email: String, name: String })(input)
if (await emailTaken(user.email)) {
  throw new Error('email already registered')
}

See also#