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

@coldsmirk/caliper-core

v0.1.0

Published

Framework-agnostic JSON Schema tree model: parse, serialize, and infer the draft 2020-12 subset a visual schema builder can host losslessly.

Readme

@coldsmirk/caliper-core

The JSON Schema tree model behind a visual schema builder: project a schema document onto an id-tagged field tree, serialize it back, and infer one from a sample payload. Pure TypeScript — no React, no DOM, no UI framework, and zero runtime dependencies.

Part of caliper. For the visual builders on top of this model, add @coldsmirk/caliper-mantine.

What you get

  • Parse a draft 2020-12 schema document into a field tree — or a structured refusal naming exactly what put it out of reach.
  • Serialize that tree back to pretty-printed schema JSON, preserving what the source declared (and what it deliberately did not).
  • Infer a schema from a sample payload, conservatively.
  • Strong typing end to end, dual ESM / CJS, no dependencies to audit.

Install

pnpm add @coldsmirk/caliper-core

Node.js >= 24 for build / SSR hosts; browsers need ES2022 (Object.hasOwn) — the bundles ship untranspiled.

Quick start

import { inferSchema, parseSchemaTree, serializeSchemaTree } from "@coldsmirk/caliper-core";

const inferred = inferSchema({
  amount: 120,
  customer: { name: "Ada" },
  tags: ["priority"]
});

const parsed = parseSchemaTree(JSON.stringify(inferred));

if (parsed.ok) {
  parsed.tree.fields;                 // [{ name: "amount", type: "number", ... }, ...]
  serializeSchemaTree(parsed.tree);   // pretty-printed schema JSON, round-tripped
} else {
  parsed.issue;                       // a structured, localizable SchemaTreeIssue
}

The supported subset

The tree hosts an object-rooted draft 2020-12 subset. Anything else is refused rather than accepted-then-dropped, so a document that survives parseSchemaTree is one serializeSchemaTree can reproduce.

| Position | Keywords | | -------------- | ---------------------------------------------------------- | | Root schema | $schema, type ("object" only), properties, required, description | | Sub-schema | type, properties, required, items, description | | Field types | string, number, integer, boolean, object, array, plus type-less (any) |

$schema, when present, must be exactly SCHEMA_DIALECT_2020_12. A blank document parses as an empty tree. serializeSchemaTree emits pretty-printed JSON with a trailing newline.

Refusals

type SchemaTreeParseResult =
  | { ok: true; tree: SchemaTree }
  | { ok: false; issue: SchemaTreeIssue };

Every SchemaTreeIssue carries a code, and names the offending part where one exists — enough for a host to render a localized explanation without string-matching a message:

| Code | Payload | Raised when | | ------------------------- | ------------------- | -------------------------------------------------------------- | | invalid-json | — | The text is not valid JSON. | | root-not-object | — | The root is not a JSON object, or declares a non-object type. | | unsupported-dialect | — | $schema is present but is not the 2020-12 dialect. | | unsupported-keyword | keyword | A keyword outside the subset (oneOf, format, $ref, …). | | unsupported-field-type | field, type | A field declares a type the tree has no row for ("null", …). | | invalid-field-type | field | A field's type is not a single type name (e.g. an array). | | invalid-field-definition| field | A field's definition is not an object. | | invalid-properties | — | properties is not an object. | | invalid-required | — | required is not an array of strings. | | unknown-required-field | field | required names a property that is not declared. | | misplaced-keyword | keyword, holder | items on a non-array, or properties / required on a non-object. | | invalid-description | — | description is not a string. |

The field tree

interface SchemaTree {
  fields: SchemaTreeField[];
  dialect: boolean;         // the source declared $schema (re-emitted verbatim)
  description: string;      // the root annotation; blank means none
  explicitType?: boolean;   // the source declared type: "object"
}

interface SchemaTreeField {
  id: string;               // stable row identity for React keys — never serialized
  name: string;             // the property name under its parent object
  type: SchemaTreeFieldType;
  required: boolean;        // listed in the parent's `required`
  description: string;      // blank means none
  children: SchemaTreeField[];      // meaningful when type is "object"
  items: SchemaTreeField | null;    // meaningful when type is "array"; null = untyped elements
  explicitType?: boolean;           // the source declared this object/array type
  preserveBlankName?: boolean;      // a blank name that came from a real property key
}

type SchemaTreeFieldType = "any" | "array" | "boolean" | "integer" | "number" | "object" | "string";

explicitType preserves applicability, not just visible fields. A schema with properties or items but no type still permits instances of other types, so the parser records that omission and serialization does not add a type the source never had. A hand-built tree that omits the flag keeps the builder default and emits the explicit type.

Property names are arbitrary JSON strings. Empty, whitespace-only, and prototype-like names such as __proto__ round-trip intact. A parsed blank name carries preserveBlankName: true; a blank row from newSchemaTreeField() has no marker and stays an unfinished UI draft, omitted from serialization. Update parsed nodes immutably (object spread) so these source-preservation flags survive an edit.

Serialization is last-writer-wins for duplicate names. Two rows sharing a name collapse to one properties entry — the last one, for both the property and its required membership. A builder UI should flag the collision inline; the model does not invent a disambiguation.

An any-typed, description-less array element is not emitted. It constrains nothing, so it serializes as a bare { "type": "array" } — the same meaning, without a noise keyword.

Inference

function inferSchema(sample: Json): { [key: string]: Json };

Deliberately conservative, because a single sample is weak evidence:

  • Integer samples infer number — a 5 rarely promises integers forever.
  • Nothing is marked required — validation strictness is an authoring decision.
  • null says nothing about the real type, so it infers {} (any).
  • Array elements merge to their loosest common schema: same-typed objects union their properties, same-typed arrays merge their items, and anything else collapses to {}. An empty array leaves items open.

The output is always inside the subset parseSchemaTree hosts, so inference feeds the tree directly.

API reference

function parseSchemaTree(text: string): SchemaTreeParseResult;
function serializeSchemaTree(tree: SchemaTree): string;
function inferSchema(sample: Json): { [key: string]: Json };
function newSchemaTreeField(overrides?: Partial<Omit<SchemaTreeField, "id">>): SchemaTreeField;
const SCHEMA_DIALECT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
  • FunctionsparseSchemaTree, serializeSchemaTree, inferSchema, newSchemaTreeField
  • ConstantsSCHEMA_DIALECT_2020_12
  • TypesJson, SchemaTree, SchemaTreeField, SchemaTreeFieldType, SchemaTreeIssue, SchemaTreeParseResult

newSchemaTreeField mints the ids a keyed UI list needs — a tagged counter, not a UUID, because field identity is a UI concern with no persistence or cross-process meaning. The tag is randomized per module copy, so two copies of this package in one bundle cannot collide inside a single tree.

License

UNLICENSED — proprietary. All rights reserved; no use, copying, or redistribution without the author's permission.