npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

athlete-json-validator

v0.6.0

Published

TypeScript-compatible JSON schema validation library with strong type inference and zero dependencies

Readme

athlete-json-validator

Schema validation for plain data with strong TypeScript type inference. Zero dependencies.

Define a shape with constructor tokens, get back a typed, whitelisted copy of the input:

import { Validator } from "athlete-json-validator";

const validator = new Validator();

const userShape = {
  name: [String],
  age: [Number],
  tags: [[String]],
} as const;

const user = validator.parse(userShape, input);
// user: { name: string; age: number; tags: string[] }
// throws ValidationFailedError if input does not match

Installation

npm install athlete-json-validator

Shape syntax

A shape is a plain object. Each field holds a definition:

| Definition | Matches | | --------------------- | --------------------------------------------- | | String | string | | Number | number (NaN is rejected) | | Boolean | boolean | | Date | Date instance (for DB rows, non-JSON data) | | null | null | | undefined | undefined / missing key | | { ... } | nested object shape | | [A, B, C] | union: A or B or C | | [[A]] | array of A | | [[A, B]] | array of A \| B | | { "*": A } | record: every value must match A |

A field definition is a union list: field: [String] means "string", field: [null, String] means "null or string". A bare token without the list (field: String) is also accepted.

Inside a union list, a nested array switches meaning to "array of": tags: [[String]] reads as "union of one option: array of string".

const shape = {
  id: [String],
  score: [Number],
  note: [null, String],            // string | null
  nickname: [undefined, String],   // optional: may be absent
  labels: [[String]],              // string[]
  matrix: [[[Number]]],            // number[][]
  profile: [{                      // nested object
    theme: [String],
    flags: [[Boolean]],
  }],
  contact: [String, {              // string OR object
    email: [String],
  }],
  translations: [{ "*": [String] }], // Record<string, string>
} as const;

Records (wildcard)

A shape whose only key is "*" validates every own enumerable value of the candidate:

const dict = validator.parse({ "*": [String, Number] }, { a: "x", b: 1 });
// dict: Record<string, string | number>

Combining "*" with other keys throws UnsupportedValidatorError — a record shape describes homogeneous values and cannot mix with fixed fields.

API

new Validator(factory?)

Stateless entry point; one instance can be shared for the whole application. Compiled shapes are cached by object identity in a WeakMap, so define shapes as module-level constants and reuse them — the first call compiles, subsequent calls hit the cache.

parse(shape, candidate)

Returns the typed entity or throws:

  • ValidationFailedError (with .keys) when the candidate does not match;
  • UnsupportedValidatorError when the shape itself is invalid;
  • whatever a candidate getter throws, unchanged.
const user = validator.parse(userShape, req.body);

safeParse(shape, candidate)

Never throws for data problems; returns a discriminated result:

const result = validator.safeParse(userShape, req.body);
if (result.success) result.data;        // typed entity
else if ("keys" in result) result.keys; // error paths
else result.error;                      // exception thrown by the candidate itself

Invalid shapes still throw UnsupportedValidatorError — a broken schema is a programmer error, not a data error.

validate(shape, candidate)

Returns a plain boolean and narrows the candidate to the inferred type — nothing is extracted, the value keeps its own identity and its extra keys:

const payload: unknown = await request.json();

if (validator.validate(userShape, payload)) {
  payload.name.toUpperCase(); // payload is InferEntity<typeof userShape>
}

Use it when you only need the type guard; use parse/safeParse when you want a whitelisted copy.

validateArray(shape, candidate)

The same guard for arrays: true only if the candidate is an array and every element matches. A non-array candidate is false, not an exception:

if (validator.validateArray(userShape, rows)) {
  rows.forEach((row) => row.age); // rows is InferEntity<typeof userShape>[]
}

Both guards still throw UnsupportedValidatorError for a broken shape, and propagate exceptions thrown by the candidate's own getters.

parseArray(shape, candidate) / safeParseArray(shape, candidate)

Validate an array of entities against one shape. A non-array candidate produces CandidateNotArrayError. On failure, keys holds one group per element, empty for valid elements:

validator.safeParseArray(shape, [ok, badValue, badId]);
// { success: false, keys: [[], ["value"], ["id"]] }

Extraction semantics

parse returns a fresh copy containing only the keys declared in the shape:

  • extra fields of the candidate are stripped;
  • keys absent from the candidate are omitted from the result (relevant for [undefined, ...] unions);
  • containers (objects, arrays) are newly built; primitive values and Date instances are carried over by reference;
  • only own enumerable properties are read — values from a prototype chain are treated as absent;
  • every property is read exactly once: validation and extraction happen in a single pass, so a getter cannot return one value to the validator and another to the result.

For unions the first matching option wins and drives extraction:

validator.parse({ f: [{ a: [String] }, { b: [Number] }] }, { f: { a: "x", b: 1 } });
// { f: { a: "x" } } — first option matched, extracted through it

Arrays and Date instances are rejected where an object shape is expected — they are type mismatches, not objects.

Error paths

keys uses dot notation for nested fields and array indices. The root is addressed by the empty string:

validator.safeParse(shape, { users: [{ name: 1 }] });
// keys: ["users.0.name"]

validator.safeParse(shape, null);
// keys: [""]

A failed union contributes one nested group per option:

validator.safeParse({ f: [{ s: [String] }, { n: [Number] }] }, { f: {} });
// keys: [["f.s"], ["f.n"]]

Type inference

Shape → type: InferEntity<T>

import type { InferEntity } from "athlete-json-validator";

const shape = { id: [String], note: [undefined, String] } as const;
type Entity = InferEntity<typeof shape>;
// { id: string; note?: string | undefined }

Fields whose union admits undefined become optional — matching the runtime behaviour of omitting absent keys.

Type → shape: Shape<T>

import type { Shape } from "athlete-json-validator";

type Account = {
  id: string;
  amount: number;
  labels: string[];
};

const accountShape: Shape<Account> = {
  id: [String],
  amount: [Number],
  labels: [[String]],
};

Shape<T> works with type aliases composed of JSON-compatible values (plus Date). Interfaces need an index signature to qualify.

The options of a field may be listed in any order[String, undefined] and [undefined, String] are equally valid — but the set must stay exhaustive: POOL_MAX: [String] for POOL_MAX?: string is a type error, because at runtime the key would become required.

License

ISC

Author

Denis Redcade