Schema ValidatorLoading saved progress…

Schema Validator

Build a tiny composable schema-validation library — the seed of a tool like Zod or Yup. You expose a namespace of builders that each return a validator: s.string(), s.number(), s.boolean() for primitives, and s.object({ ... }) to describe a flat object whose fields are themselves primitive validators. Every validator answers one method — .validate(value) — returning { valid, errors }, where errors is a list of human-readable messages that is empty exactly when the value is valid. The whole point is composition: an object schema isn't a special case, it's just a map of smaller validators, each run on its own field, with the errors collected.

Signature

// A validator is any object with a validate method:
//   type Result    = { valid: boolean; errors: string[] };  // errors empty ⇔ valid
//   type Validator = { validate(value: unknown): Result };

const s = schemaValidator; // the exported namespace of builders

s.string():  Validator   // valid when typeof value === 'string'
s.number():  Validator   // valid when value is a real, finite number (NaN is NOT valid)
s.boolean(): Validator   // valid when value === true or value === false

// A flat object schema: each field maps to a primitive validator above.
s.object(shape: Record<string, Validator>): Validator
//   Runs each field's validator against value[field] and AGGREGATES the
//   errors, prefixing each with the field name, e.g. 'age: expected number'.

Examples

const s = schemaValidator;

// A primitive validator: pass and fail.
s.string().validate('hello');
// → { valid: true,  errors: [] }
s.number().validate('42');
// → { valid: false, errors: ['expected number'] }
const s = schemaValidator;

// An object schema with two bad fields — BOTH are reported, each named.
const user = s.object({ name: s.string(), age: s.number(), admin: s.boolean() });
user.validate({ name: 99, age: 'old', admin: true });
// → {
//     valid: false,
//     errors: ['name: expected string', 'age: expected number'],
//   }

Notes

  • One result shape everywhere. Every validator returns { valid, errors }. errors is a string[] that is empty when valid is true and has at least one message when valid is false. s.object depends on this consistency to combine its fields.
  • NaN is not a valid number. typeof NaN === 'number', but it almost never means what the caller wants, so s.number().validate(NaN) must be invalid. Decide with Number.isFinite, not a bare typeof.
  • false is a valid boolean. Don't gate on truthiness — s.boolean().validate(false) is valid. A if (value) check would wrongly reject it.
  • A missing field is just undefined. If value has no age key, that field's validator receives undefined and fails with its normal type error ('age: expected number'). You don't need a separate "missing key" code path.
  • s.object only accepts an actual object. Passing null, an array, or a primitive to an object validator is invalid — report a single top-level 'expected object' rather than crawling fields. (typeof null === 'object', so guard it explicitly.)
  • Scope: primitives + one flat level. No chainable .min() / .max() constraints, and no nested object / array / optional support — those are separate, harder problems. Keep each field a single primitive validator.
Loading editor…