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

snap-json-llm

v0.1.0

Published

Structured outputs for LLMs: force OpenAI, Anthropic Claude & Google Gemini to return valid JSON matching your schema. Zero-dependency Instructor alternative with JSON Schema enforcement, streaming partial JSON parsing, auto-repair & self-healing retries.

Readme

snap-json-llm

Force any LLM to return strictly valid JSON. One tiny API for OpenAI (GPT-4o), Anthropic Claude, and Google Gemini structured outputs — with streaming partial objects, automatic JSON repair, and self-healing retries. Zero dependencies. No Zod required.

npm install snap-json-llm

The problem: LLMs wrap JSON in markdown fences, cut it off mid-string, miss commas, invent keys, and return "36" when you asked for a number.

The fix: snap-json constrains the generation and guarantees the result — you get a typed object matching your schema, or a typed error. Never malformed output.

import { SnapJson } from 'snap-json-llm';

const client = new SnapJson({ apiKeys: { openai: process.env.OPENAI_API_KEY } });

const person = await client.extract<{ name: string; age: number }>(
  'Ada Lovelace was 36 when she died.',
  { provider: 'openai', model: 'gpt-4o', schema: { name: 'string', age: 'number' } },
);
// ✅ { name: 'Ada Lovelace', age: 36 }  — always. Or a SnapJsonError. Never garbage.

Why snap-json?

| | snap-json | Zod + Instructor-style | Raw SDK calls | |---|---|---|---| | Runtime dependencies | 0 | Zod + provider SDKs | provider SDK each | | Schema definition | Plain object template | Zod chains | Hand-written JSON Schema | | OpenAI + Claude + Gemini | ✅ one API | varies | 3 different APIs | | Streaming partial objects | ✅ | varies | ❌ raw text deltas | | Broken-output recovery | Free local repair → snippet-only healing | Full prompt retry 💸 | ❌ | | Runs on | Node ≥18 · Bun · Deno · browsers · edge | Node mostly | varies |


Table of contents


Quick start

import { SnapJson } from 'snap-json-llm';

const client = new SnapJson({
  apiKeys: {
    openai: process.env.OPENAI_API_KEY,       // only the providers you use
    anthropic: process.env.ANTHROPIC_API_KEY,
    gemini: process.env.GEMINI_API_KEY,
  },
  defaultProvider: 'openai',                  // optional
});

interface Invoice {
  vendor: string;
  total: number;
  currency: 'USD' | 'EUR' | 'GBP';
  lineItems: { description: string; amount: number }[];
}

const invoice = await client.extract<Invoice>(emailBody, {
  model: 'gpt-4o',
  schema: {
    vendor: 'the company that issued the invoice',
    total: 'number',
    currency: 'enum:USD|EUR|GBP',
    lineItems: [{ description: 'string', amount: 'number' }],
  },
});

Switching providers is one word — the schema, the call, and the result shape stay identical:

await client.extract<Invoice>(emailBody, { provider: 'anthropic', model: 'claude-sonnet-5', schema });
await client.extract<Invoice>(emailBody, { provider: 'gemini', model: 'gemini-2.5-flash', schema });

Schemas with zero boilerplate

Describe the shape you want as a plain object. Values are hints:

| You write | You get | |---|---| | 'string' 'number' 'integer' 'boolean' | that exact type | | 'number?' (any hint + ?) | nullable field | | 'enum:red\|green\|blue' | string restricted to those values | | 'the customer's full name' | a string with that description (guides the model!) | | 42, true | type inferred from the example | | ['string'] / [{ id: 'integer' }] | array of that item shape | | { nested: { ... } } | nested object, all fields required |

schema: {
  title: 'string',
  sentiment: 'enum:positive|neutral|negative',
  confidence: 'number',
  summary: 'a one-sentence summary in plain English',
  mentions: [{ name: 'string', role: 'string?' }],
}

A minimal JSON Schema subset is accepted anywhere a template is:

schema: {
  type: 'object',
  properties: {
    status: { type: 'string', enum: ['open', 'closed'] },
    count: { type: 'integer' },
    note: { type: 'string', nullable: true },
  },
  required: ['status', 'count'],
  additionalProperties: false,
}

Supported keywords: type, properties, required, items, enum, description, nullable, additionalProperties.


Streaming partial objects

streamObject gives you valid, parseable partial objects on every change while the model is still generating — not raw text deltas. Perfect for streaming UIs.

const article = await client.streamObject<Article>(
  'Write a product announcement…',
  { provider: 'anthropic', model: 'claude-sonnet-5', schema: { headline: 'string', paragraphs: ['string'] } },
  (partial) => {
    // Fires as the JSON grows. Every partial is real data:
    // { headline: 'Introd' }
    // { headline: 'Introducing snap-json' }
    // { headline: 'Introducing snap-json', paragraphs: ['LLMs are...'] }
    render(partial);
  },
);
// resolves with the complete, validated object

How it stays valid mid-stream: a single-pass balancer closes dangling strings, drops half-typed keys (never inventing data), trims incomplete numbers/literals, and balances brackets — so JSON.parse succeeds on every snapshot.

Options: pass emitOnlyOnChange: false to receive a callback on every token delta instead of only when the object changes.


The JSON guarantee pipeline

Every response passes through four stages, ordered cheapest-first:

1. CONSTRAIN   the request itself forces JSON output
               (OpenAI strict json_schema · Claude forced tool call · Gemini responseSchema)
        │
2. REPAIR      free, local, instant — fixes truncation, markdown fences,
               trailing prose, unbalanced brackets
        │
3. HEAL        only if still broken: the invalid SNIPPET + the exact error
               go to a cheap model (gpt-4o-mini / Claude Haiku / Gemini Flash).
               Your original prompt is NEVER re-run. 💰
        │
4. VALIDATE    types, required keys, enums checked; unknown keys pruned free;
               per-field healing for mismatches; then → your typed object
               or a SnapJsonError with structured issues

Configuration

Client

const client = new SnapJson({
  apiKeys: { openai: '...', anthropic: '...', gemini: '...' },
  defaultProvider: 'openai',                       // used when a call omits `provider`
  baseUrls: { openai: 'https://my-gateway/v1' },   // proxies & gateways
  fetch: myCustomFetch,                            // testing, middleware, retries
  healing: { model: 'gpt-4o-mini', maxAttempts: 2 },
});

Per-request options (extract and streamObject)

| Option | Type | Default | What it does | |---|---|---|---| | provider | 'openai' \| 'anthropic' \| 'gemini' | client default | which API to call | | model | string | — (required) | model id | | schema | template or JSON Schema | — (required) | target shape | | system | string | — | system prompt (mapped to each provider's native field) | | temperature | number | provider default | sampling temperature | | maxOutputTokens | number | 4096 | output cap | | schemaName | string | 'emit_json' | tool / json_schema envelope name | | signal | AbortSignal | — | cancellation | | healing | object | see below | per-request healing override | | emitOnlyOnChange | boolean | true | (streaming only) callback gating |

Healing options

healing: {
  enabled: true,          // false = throw instead of spending tokens
  maxAttempts: 2,         // snippet-repair round-trips
  provider: 'openai',     // where corrective calls go (default: same as request)
  model: 'gpt-4o-mini',   // default per provider: gpt-4o-mini / claude-haiku / gemini-flash
  snippetRadius: 160,     // context chars sent around the error position
}

Error handling

Everything throws SnapJsonError — one type to catch, with structured fields:

import { SnapJson, SnapJsonError } from 'snap-json-llm';

try {
  const data = await client.extract<Order>(text, options);
} catch (error) {
  if (error instanceof SnapJsonError) {
    error.provider;   // 'openai' | 'anthropic' | 'gemini'
    error.status;     // HTTP status when the API call itself failed (e.g. 429)
    error.issues;     // ValidationIssue[] — [{ path: '$.total', message: 'expected number, got string' }]
    error.rawOutput;  // the model's original text, for logging
  }
}

You'll get an error (not silent bad data) when: the API call fails, the model refuses, or output can't be made schema-valid within the healing budget.


Low-level utilities

Every pipeline stage is exported for building your own flows:

import {
  repairJson,           // (text) => balanced, parseable JSON string — pure, instant
  parseStreamChunk,     // stateful: feed deltas, get the latest partial object
  StreamingJsonParser,  // class version with .raw, .reset(), isolated state
  SchemaTranslator,     // template → provider-native payload fragments
  validatePayload,      // (value, schema) => ValidationIssue[]
  healPayload,          // (brokenJson, errorMsg, fallbackClient) => fixed value
  pruneUnknownKeys,     // free local cleanup of forbidden keys
} from 'snap-json-llm';

repairJson('{"items": [1, 2');          // → '{"items": [1, 2]}'
repairJson('```json\n{"a": 1}\n```');   // → '{"a": 1}'

Provider details

| | OpenAI | Anthropic Claude | Google Gemini | |---|---|---|---| | Mechanism | response_format: json_schema (strict mode) | forced tool call (tool_choice: {type: 'tool'}) | responseMimeType + responseSchema | | Streaming | delta.content SSE | input_json_delta SSE | alt=sse chunks | | Endpoint | /v1/chat/completions | /v1/messages | models/{model}:generateContent | | Auth header | Authorization: Bearer | x-api-key | x-goog-api-key |

Transport is plain fetch — no provider SDKs — so the same bundle runs on Node ≥18, Bun, Deno, browsers (Anthropic's CORS opt-in header included), and edge runtimes. Point baseUrls at any OpenAI-compatible gateway.


FAQ

Use extract with provider: 'openai'. snap-json enables OpenAI's strict structured-outputs mode (response_format: { type: 'json_schema', strict: true }) and repairs/heals anything that still slips through — truncation at the token limit being the common one.

Not natively. snap-json uses Anthropic's recommended pattern — a forced tool call whose input_schema is your shape — and normalizes the tool input back to a plain object, so Claude behaves exactly like the other providers.

Yes — repairJson (pure function) and parseStreamChunk / StreamingJsonParser (stateful) work standalone on any partial JSON text, no API keys involved.

Usually nothing: constrained generation plus free local repair handles the typical failures. When a paid heal does trigger, it sends ~a few hundred tokens (the broken snippet + error rule) to a budget model — not your full prompt re-run. Set healing: { enabled: false } for a strict throw-on-invalid mode with zero extra calls.

Built in strict TypeScript. extract<T> / streamObject<T> return your interface directly, Partial<T> flows through streaming callbacks, and the whole package ships .d.ts declarations.

~28 kB packed, zero dependencies, tree-shakeable ESM (sideEffects: false). Import just repairJson and bundlers drop the rest.


License

MIT