JavaScript type detection + schema validation (JSON Schema draft 2020-12 subset).
Module valid | Source packages/front/fw/src/io/utils/valid.js | Deps none | Worker-safe yes
Two distinct surfaces:
- Type guards —
is,isNumber,isString, … (unchanged, used internally bybuffer). - Schema validation —
validate,test,compilefor payload/form/options.
Resolve
const valid = runtime.resolve('valid');
// Returns: { is, isNumber, isString, isBoolean, isArray, isObject,
// isUint8Array, isUint8ClampedArray, isFunction,
// validate, test, compile, formats, __map }
API — Type guards
| Method | Signature | Description |
|---|---|---|
is |
(obj) => string |
Lowercase type name (e.g. "number", "uint8array") |
isNumber |
(obj) => boolean |
typeof obj === 'number' && Number.isFinite(obj) (strict: excludes NaN, Infinity, strings) |
isString |
(obj) => boolean |
[object String] |
isBoolean |
(obj) => boolean |
true, false, [object Boolean] |
isArray |
(obj) => boolean |
[object Array] |
isObject |
(obj) => boolean |
[object Object] (plain object) |
isUint8Array |
(obj) => boolean |
[object Uint8Array] |
isUint8ClampedArray |
(obj) => boolean |
[object Uint8ClampedArray] |
isFunction |
(obj) => boolean |
[object Function] |
API — Schema validation
| Method | Signature | Returns |
|---|---|---|
validate |
(value, schema, options?) => {valid, errors} |
Detailed error list |
test |
(value, schema, options?) => boolean |
Boolean short-circuit |
compile |
(schema, options?) => {validate, test} |
Validator pre-bound to a schema |
formats |
Object<string, RegExp> |
Built-in formats accessible for reading |
Options
{ formats?: Object<string, RegExp | (v: string) => boolean> }
Custom formats extend or override built-ins.
Supported keywords
| Category | Keywords |
|---|---|
| Type | type, nullable, const, enum |
| String | minLength, maxLength, pattern, format |
| Number | minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf |
| Array | items, minItems, maxItems, uniqueItems |
| Object | properties, required, additionalProperties, patternProperties |
| Combinators | allOf, anyOf, oneOf, not |
Accepted JSON Schema types: 'string', 'number', 'integer', 'boolean', 'null', 'object', 'array', or an array of these values (union).
Built-in formats
| Format | Example | Reference |
|---|---|---|
email |
user+tag@example.co.uk |
WHATWG HTML5 §4.10.5.1.5 + RFC 5321 limits |
url |
https://example.com/a |
Pragmatic (http/https scheme) |
uuid |
550e8400-e29b-41d4-a716-446655440000 (v1–v5) |
RFC 4122 |
date |
2024-01-15 |
ISO 8601 |
datetime |
2024-01-15T10:30:00Z or …+02:00 |
ISO 8601 |
ipv4 |
192.168.1.1 |
RFC 791 |
ipv6 |
2001:db8::1, ::ffff:192.0.2.1, fe80::1%eth0 |
RFC 4291 + RFC 6874 zone ID |
hex |
deadBEEF |
— |
base64 |
dGVzdA== |
RFC 4648 |
Email — full validation:
- Local-part character set RFC 5322 (alphanum +
.!#$%&'*+/=?^_{|}~-`). - Strict dot-atom RFC 5321 §4.1.2: no leading, trailing, or consecutive
.. - Domain LDH labels (letter/digit/hyphen), hyphen never at boundaries, label ≤ 63 chars.
- Length limits RFC 5321 §4.5.3.1: local ≤ 64, domain ≤ 255, total ≤ 254.
- TLD ≥ 2 chars.
- Quoted local-parts (
"..."@) and IDN/SMTPUTF8 (Unicode) are intentionally rejected — not supported by the vast majority of providers and a classic source of injection bugs. For IDN, provide a customformat. - ReDoS-safe regex (no exponential backtracking).
IPv6 — full RFC 4291 coverage:
- Preferred form
x:x:x:x:x:x:x:x(8 groups of 1–4 hex). - Compressed form
::(only one per address):::,::1,2001:db8::,2001:db8::1. - IPv4-mapped:
::ffff:192.0.2.1,::ffff:0:192.0.2.1(RFC 4291 §2.5.5.2). - IPv4-translated (legacy):
::192.0.2.1(§2.5.5.1). - IPv4-embedded:
2001:db8:3:4::192.0.2.1,64:ff9b::192.0.2.1(RFC 6052). - Link-local zone ID:
fe80::1%eth0,fe80::%1(RFC 6874). - Uppercase hex accepted.
- ReDoS-safe (non-overlapping alternatives).
An unknown format is silently ignored ("annotation only" behavior per JSON Schema default).
Examples
Type guards
valid.is(42); // "number"
valid.is(new Uint8Array()); // "uint8array"
valid.isObject({ x: 1 }); // true
valid.isNumber(5); // true
valid.isNumber('5'); // false — strict (typeof + isFinite)
Simple validation
valid.test('hello', { type: 'string' }); // true
valid.test(42, { type: 'integer', minimum: 0 }); // true
valid.test(-1, { type: 'integer', minimum: 0 }); // false
Application object (user creation payload)
const userSchema = {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string', minLength: 1, maxLength: 50 },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0, maximum: 150 },
tags: { type: 'array', items: { type: 'string' }, uniqueItems: true },
role: { enum: ['admin', 'user', 'guest'] }
},
additionalProperties: false
};
const { valid: ok, errors } = valid.validate(
{ name: '', email: 'bad', age: -5 },
userSchema
);
// ok === false
// errors: [
// { path: '/email', keyword: 'required', ... }, // if email was missing
// { path: '/name', keyword: 'minLength', ... },
// { path: '/email', keyword: 'format', ... },
// { path: '/age', keyword: 'minimum', ... }
// ]
Arrays and composite types
// Homogeneous array
valid.test([1, 2, 3], { type: 'array', items: { type: 'integer' } }); // true
// Type union
valid.test('x', { type: ['string', 'number'] }); // true
valid.test(null, { type: 'string', nullable: true }); // true
Combinators
// String OR number
valid.test(42, { anyOf: [{ type: 'string' }, { type: 'number' }] }); // true
// Non-empty string AND length ≤ 10
const s = { allOf: [{ type: 'string' }, { minLength: 1 }, { maxLength: 10 }] };
// Exactly one of the two
valid.test('x', { oneOf: [{ type: 'string' }, { type: 'number' }] }); // true
// Negation
valid.test(42, { not: { type: 'string' } }); // true
Custom format
const schema = { type: 'string', format: 'slug' };
const opts = { formats: { slug: /^[a-z0-9-]+$/ } };
valid.test('my-slug', schema, opts); // true
valid.test('Not A Slug', schema, opts); // false
// Function instead of regex
valid.test('42', { type: 'string', format: 'even' }, {
formats: { even: s => parseInt(s, 10) % 2 === 0 }
});
Pre-compilation (hot path)
const userCheck = valid.compile(userSchema);
for (const user of users) {
if (!userCheck.test(user)) {
console.warn('invalid', userCheck.validate(user).errors);
}
}
patternProperties / additionalProperties
// All keys starting with x_ must be numeric
valid.test(
{ x_a: 1, x_b: 2, name: 'ok' },
{
type: 'object',
properties: { name: { type: 'string' } },
patternProperties: { '^x_': { type: 'number' } }
}
); // true
// Strict: reject any undeclared property
valid.test(
{ a: 1, unexpected: 2 },
{ type: 'object', properties: { a: { type: 'number' } }, additionalProperties: false }
); // false
Error structure
Each element of errors:
{
path: '/user/email', // JSON-Pointer-like
keyword: 'format', // keyword that failed
message: 'does not match format email'
}
The path is always of the form '/<segment>/<segment>...' (root = '/').
Worker Usage
const worker = fw.createWorker(
function ({ libs, args }) {
const [value, schema] = args;
const result = libs.valid.validate(value, schema);
self.postMessage(result);
},
{ dependencies: ['valid'], args: [payload, mySchema] }
);
Notes
- No
$ref/$defs/ JSON-Pointer — for recursive or catalogued schemas, introduce a dedicatedschemamodule based on a third-party validator (Ajv, Zod-like). isNumberrequirestypeof === 'number' && isFinite— excludesNaN,Infinity, and any string. For numeric strings, use{ type: 'number' }viavalidate.NaN/Infinityalways fail{ type: 'number' }(only finite values accepted).integeris a subset ofnumber:{ type: 'number' }accepts42and1.5,{ type: 'integer' }accepts only42.constandenumuse deep equality (arrays and objects compared recursively).uniqueItemsalso uses deep equality — costly on large arrays (O(n²)); pre-compiling the schema does not speed up this step (structurally quadratic).multipleOfuses a 1e-12 tolerance to support0.1 * 3vs0.3— sufficient for application data, not for scientific computation.additionalProperties: true(default) = all accepted;false= strict rejection; object = schema applied to undeclared properties.