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

@formloom/schema

v0.4.0

Published

Schema spec and validator for Formloom - LLM-driven dynamic form generation

Readme

@formloom/schema

The core schema specification for Formloom. TypeScript types, constants, and a validator. Zero runtime dependencies.

Installation

npm install @formloom/schema

Schema Spec

A Formloom schema is a JSON object with this shape:

{
  "version": "1.2",
  "title": "Job Application",
  "description": "Tell us a bit about yourself",
  "submitLabel": "Submit",
  "fields": [
    {
      "id": "email",
      "type": "text",
      "label": "Email Address",
      "placeholder": "[email protected]",
      "validation": {
        "required": true,
        "pattern": "^[^@]+@[^@]+\\.[^@]+$",
        "patternMessage": "Must be a valid email"
      }
    },
    {
      "id": "years_experience",
      "type": "number",
      "label": "Years of experience",
      "validation": { "min": 0, "max": 60, "integer": true, "required": true }
    },
    {
      "id": "employment_type",
      "type": "radio",
      "label": "Employment type",
      "options": [
        { "value": "full_time", "label": "Full-time" },
        { "value": "contract", "label": "Contract" }
      ],
      "validation": { "required": true }
    },
    {
      "id": "day_rate",
      "type": "number",
      "label": "Day rate (USD)",
      "showIf": { "field": "employment_type", "equals": "contract" }
    },
    {
      "id": "resume",
      "type": "file",
      "label": "Resume",
      "accept": "application/pdf",
      "maxSizeBytes": 2000000,
      "validation": { "required": true }
    }
  ],
  "sections": [
    { "id": "details", "title": "Details", "fieldIds": ["email", "years_experience"] },
    { "id": "role", "title": "Role", "fieldIds": ["employment_type", "day_rate"] },
    { "id": "docs", "title": "Documents", "fieldIds": ["resume"] }
  ]
}

Field types

These are rendering primitives, not semantic types. A text field with an email regex IS an email field.

| Type | Description | Extra properties | |------|-------------|------------------| | text | Single-line text input | placeholder, defaultValue (string) | | boolean | Yes/no toggle | defaultValue (boolean) | | radio | Pick one from options | options (required), defaultValue (string) | | select | Dropdown or multi-select | options (required), multiple, placeholder, defaultValue (string / string[]) | | date | Date picker | placeholder, defaultValue (ISO 8601 string) | | number | Numeric input | placeholder, defaultValue (number), validation: min, max, step, integer | | file | File upload | accept (MIME globs), maxSizeBytes, multiple |

Validation rules

| Property | Type | Applies to | Description | |----------|------|------------|-------------| | required | boolean | all | Field must be filled | | pattern | string | text, date | Regex (no delimiters). Catastrophic patterns are rejected. | | patternMessage | string | text, date | Error message on pattern failure | | min, max | number | number | Inclusive range | | step | number | number | Granularity; must be > 0 | | integer | boolean | number | Reject non-integers |

Conditional visibility (showIf)

Attach a showIf rule to any field. Hidden fields are excluded from submitted data.

// leaf rules
{ "field": "country", "equals": "US" }
{ "field": "plan", "in": ["pro", "enterprise"] }
{ "field": "coupon", "notEmpty": true }

// composites
{ "allOf": [ ... ] }
{ "anyOf": [ ... ] }
{ "not": { ... } }

Dependency cycles and references to unknown fields are caught at validation time.

The package exports three helpers so custom renderers and servers can share the same semantics as the React hook:

import {
  evaluateShowIf,
  collectShowIfDependencies,
  findShowIfCycle,
} from "@formloom/schema";

evaluateShowIf(field.showIf, currentData);     // boolean
collectShowIfDependencies(field.showIf);       // Set<string> of referenced field ids
findShowIfCycle(schema.fields);                // string[] (e.g. ["a","b","a"]) or null

Sections

Optional top-level grouping. When sections is present, every field id must belong to exactly one section.

"sections": [
  { "id": "personal", "title": "Personal", "fieldIds": ["name", "email"] },
  { "id": "employment", "fieldIds": ["role", "start_date"] }
]

Rendering hints

Hints are advisory — renderers honour what they know and ignore the rest. The canonical registry is exported as CANONICAL_HINTS:

| Key | Values | Meaning | |-----|--------|---------| | display | "textarea", "password", "toggle", "stepper" | Ask for a non-default widget | | width | "full", "half", "third" | Layout hint | | rows | integer | Textarea row count | | autocomplete | HTML autocomplete token | Browser autofill hint |

Unknown hints pass through, so you can extend without a version bump.

Validator

import { validateSchema } from "@formloom/schema";

const result = validateSchema(maybeSchema);

if (result.valid) {
  // result.errors is []; result.warnings may contain non-fatal notices
} else {
  // result.errors is ValidationError[] with { path, message }
  console.log(result.errors);
}

Collects all errors rather than failing on the first one.

If you prefer a throw-on-invalid style, wrap the result:

import { validateSchema, FormloomValidationError } from "@formloom/schema";

const result = validateSchema(maybeSchema);
if (!result.valid) throw new FormloomValidationError(result.errors);

FormloomValidationError extends Error with an errors: ValidationError[] property and a pre-formatted message listing every issue.

Forward compatibility

A v1.5 schema emitted by a newer LLM may contain a field type this runtime doesn't know. Control the behaviour via the forwardCompat option:

validateSchema(schema, { forwardCompat: "lenient" });
// → unknown field types are dropped (with a warning) and listed in result.droppedFields
// → `"strict"` (default) errors out

Capabilities (v1.3)

Pass a FormloomCapabilities object to restrict what the schema may contain on a given surface. Disallowed field types and features become validation errors in strict mode or silent drops with warnings in lenient mode (same knob as forwardCompat).

validateSchema(schema, {
  capabilities: {
    fieldTypes: ["text", "select", "boolean"],   // no file, number, date, radio
    features: { showIf: false },                 // reject conditional fields
    variants: ["combobox"],                      // hints.variant allowlist
    maxFields: 7,
  },
  forwardCompat: "strict",
});

Omit any key to allow it (omit = permissive). The error paths match the existing convention (fields[2].type, fields[0].hints.variant, etc.) so surfaces are greppable. For the ergonomic path — narrowing the system prompt and tool JSON Schema from the same declaration — use createFormloomCapabilities in @formloom/llm.

Versioning

Any 1.x version string is accepted. Export constants:

FORMLOOM_SCHEMA_VERSION          // "1.2"
FORMLOOM_MIN_SUPPORTED_VERSION   // "1.0"
FIELD_TYPES                      // readonly ["text","boolean","radio","select","date","number","file"]

Helpers for working with version strings:

import {
  parseSchemaVersion,
  isSupportedVersion,
  compareVersions,
} from "@formloom/schema";

parseSchemaVersion("1.2");       // { major: 1, minor: 2 }
parseSchemaVersion("not-a-ver"); // null — malformed input returns null instead of throwing

isSupportedVersion("1.3", 1);    // true  — same major
isSupportedVersion("2.0", 1);    // false — different major

compareVersions({ major: 1, minor: 2 }, { major: 1, minor: 0 }); // positive: a > b

Safe regex

LLM-authored regex patterns are passed to safeRegexTest, which detects catastrophic shapes (nested quantifiers, overlapping alternations, backrefs) and caps input length. It never hangs and never throws.

import {
  safeRegexTest,
  isCatastrophicPattern,
  isValidRegexSyntax,
  DEFAULT_MAX_INPUT_LENGTH,
} from "@formloom/schema";

const result = safeRegexTest(pattern, value);
// { matched: boolean, skipped: boolean, reason?: string }
//
// skipped = true when:
//   - the pattern is syntactically invalid
//   - the pattern matches a known catastrophic shape
//   - the input exceeds DEFAULT_MAX_INPUT_LENGTH (10 KB by default)

isCatastrophicPattern("(a+)+");       // true
isValidRegexSyntax("^[a-z]+$");       // true

Pass { maxInputLength } as the third argument to change the cap per-call.

File matching

Every layer of Formloom (validator, React hook, Zod adapter) uses the same accept-string matcher, so behaviour is identical across the stack.

import { fileMatchesAccept, mimeMatches } from "@formloom/schema";

fileMatchesAccept("image/*,.pdf", "image/png", "photo.png");      // true
fileMatchesAccept("image/*,.pdf", "application/pdf", "cv.pdf");   // true (via .pdf)

// mimeMatches is MIME-only — extension tokens are ignored because
// there is no filename context. Prefer fileMatchesAccept when you have a name.
mimeMatches("image/png", "image/*");         // true
mimeMatches("application/pdf", ".pdf");      // false

TypeScript types

import type {
  // Schema shape
  FormloomSchema,
  Section,
  FieldType,
  BaseField,
  FormField,
  TextField, BooleanField, RadioField, SelectField,
  DateField, NumberField, FileField,

  // Values
  FormloomData, FormloomFieldValue, FormloomFileValue,

  // Per-field bits
  FieldOption, ValidationRule, NumberValidationRule,
  RenderHints,
  CanonicalHints, CanonicalHintEntry, CanonicalDisplayHint, CanonicalWidthHint,
  FieldHints,
  ShowIfRule,

  // Capabilities (v1.3)
  FormloomCapabilities, ResolvedFeatures,

  // Validator
  ValidationResult, ValidationError, ValidationWarning, ValidateOptions,

  // Version helpers
  ParsedVersion,

  // Safe regex
  SafeRegexOptions, SafeRegexResult,
} from "@formloom/schema";

What's new in v1.2

  • FieldOption.description — optional one-line sub-label for radio/select options. Turns single-word labels into two-line choices without bloating the label itself.
  • allowCustom on radio/select — opt-in "Other…" freeform input. When true, options are treated as suggestions; the submitted value may be any string (or any string[] for multiple: true). Pattern validation still applies.
    • Paired with customLabel (default "Other") and customPlaceholder for the freeform input.
    • Helpers: resolveMultiSelectValue(field, values) splits a submitted array into { selected, custom }; isRadioCustomValue(field, value) identifies a freeform radio pick.
  • hints.variant — opaque host-defined widget key. The sanctioned extension point for custom field types (see below).
  • readOnly / disabled on any field — presentation flags passed straight through to renderers. Read @formloom/react's hook-level options for the full story.

Custom field variants

When a host ships a specialized widget that's shaped exactly like an existing primitive — a multi-select with search, a tool picker, an agent autocomplete — use hints.variant instead of forging a new field type. The schema stays canonical; the renderer keys off the variant.

{
  "id": "tools",
  "type": "select",
  "label": "Tools to enable",
  "multiple": true,
  "options": [
    { "value": "jira", "label": "Jira" },
    { "value": "linear", "label": "Linear" }
  ],
  "hints": { "variant": "tool-select" }
}

Host renderer:

function FieldRenderer({ field, state, onChange }: FieldProps) {
  if (field.type === "select" && field.hints?.variant === "tool-select") {
    return <ToolSelect field={field} value={state.value} onChange={onChange} />;
  }
  // …fall through to default widgets
}

The schema validator accepts any string for variant and leaves the meaning to the host. Hosts can also declaration-merge FieldHints to get typed access:

declare module "@formloom/schema" {
  interface FieldHints {
    variant?: "tool-select" | "agent-picker" | "combobox";
  }
}

License

MIT