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

typemeld

v2.0.0

Published

Parse, validate, and repair messy LLM outputs into clean, typed data. Like zod for AI.

Downloads

299

Readme


Every developer building with LLMs hits the same wall:

You: "Return JSON with name, age, and tags"

GPT/Claude: "Sure! Here's the data:

```json
{name: 'John', age: "30", tags: "developer",}  // trailing comma, unquoted keys, wrong types

Let me know if you need anything else!"


**typemeld** fixes this in one line:

```javascript
import { parse, tm } from 'typemeld';

const user = parse(llmOutput, tm.object({
  name: tm.string(),
  age: tm.number(),
  tags: tm.array(tm.string()),
}));
// => { name: "John", age: 30, tags: ["developer"] }

It strips the markdown fences, extracts the JSON from surrounding prose, fixes the trailing comma, quotes the keys, coerces "30" to 30, wraps "developer" into ["developer"], and validates every field. One function call.

Install

npm install typemeld

Zero dependencies. Works in Node.js 18+, Bun, Deno, and browsers.

What it repairs

| LLM quirk | Example | typemeld handles it | |---|---|---| | Markdown fences | ```json { ... } ``` | ✅ Stripped | | JSON in prose | "Sure! Here's the data: { ... } Hope that helps!" | ✅ Extracted | | Trailing commas | { "a": 1, "b": 2, } | ✅ Fixed | | Unquoted keys | { name: "John" } | ✅ Quoted | | Single quotes | { 'name': 'John' } | ✅ Converted | | JS comments | { "a": 1 // comment } | ✅ Stripped | | Truncated JSON | { "items": ["one", "tw | ✅ Auto-closed | | NaN / undefined | { "value": NaN } | ✅ → null | | Infinity | { "n": Infinity } | ✅ → null | | Wrong types | { "age": "30" } with number schema | ✅ Coerced to 30 | | String → array | { "tags": "dev" } with array schema | ✅ Wrapped to ["dev"] | | String booleans | { "ok": "true" } with boolean schema | ✅ Coerced to true | | Case mismatch | { "mood": "Positive" } with enum(['positive', ...]) | ✅ Coerced to "positive" |

API

parse(input, schema?)

Parse and validate. Throws ParseError on failure.

import { parse, tm } from 'typemeld';

// Without schema — just repair JSON
const data = parse('```json\n{"key": "value",}\n```');
// => { key: "value" }

// With schema — repair + validate + coerce
const result = parse(messy_llm_output, tm.object({
  sentiment: tm.enum(['positive', 'negative', 'neutral']),
  confidence: tm.number(),
  summary: tm.string(),
}));

safeParse(input, schema?)

Same as parse but never throws. Returns { success, data, errors }.

import { safeParse, tm } from 'typemeld';

const result = safeParse(llmOutput, schema);
if (result.success) {
  console.log(result.data);
} else {
  console.log(result.errors);
  // [{ path: "confidence", message: "Expected number, got undefined", expected: "number" }]
}

repairJson(input)

Low-level JSON repair without schema validation.

import { repairJson } from 'typemeld';

repairJson("{name: 'John', age: 30,}");
// => { name: "John", age: 30 }

repairJson('Sure! Here is the data:\n{"result": true}\nHope this helps!');
// => { result: true }

repairJson('{"items": ["one", "two", "thr');
// => { items: ["one", "two", "thr"] }

extractAll(input)

Extract multiple JSON objects from a single LLM response.

import { extractAll } from 'typemeld';

const objects = extractAll('First: {"a": 1} and then {"b": 2} finally {"c": 3}');
// => [{ a: 1 }, { b: 2 }, { c: 3 }]

promptFor(schema, options?)

Generate a prompt fragment describing the expected output format.

import { promptFor, tm } from 'typemeld';

const schema = tm.object({
  sentiment: tm.enum(['positive', 'negative', 'neutral']).describe('Overall sentiment'),
  confidence: tm.number().describe('Confidence score between 0 and 1'),
  summary: tm.string().describe('One sentence summary'),
});

const systemPrompt = `Analyze the following text.\n${promptFor(schema, { strict: true })}`;
// Generates JSON Schema instructions the LLM can follow

Schema Builder

typemeld includes a lightweight, chainable schema builder. No zod dependency needed.

import { tm } from 'typemeld';

// Primitives
tm.string()
tm.number()
tm.boolean()
tm.any()

// Complex
tm.array(tm.string())                    // string[]
tm.object({ name: tm.string() })         // { name: string }
tm.enum(['a', 'b', 'c'])                 // 'a' | 'b' | 'c'

// Modifiers (chainable)
tm.string().optional()                   // string | undefined
tm.string().nullable()                   // string | null
tm.string().default('hello')             // defaults to "hello" if missing
tm.number().describe('User age in years') // description for LLM prompts

// Constraints
tm.string().min(1).max(100)              // length between 1 and 100
tm.number().min(0).max(1)                // value between 0 and 1
tm.array(tm.string()).min(1).max(10)     // array length between 1 and 10

// Object modes
tm.object({ ... })                       // strips extra keys (default)
tm.object({ ... }).passthrough()         // keeps extra keys
tm.object({ ... }).strict()              // rejects extra keys

// Transforms & refinements
tm.string().transform(s => s.trim().toLowerCase())
tm.number().refine(n => n > 0, 'Must be positive')
tm.string().preprocess(v => v ?? '')     // runs before validation
tm.string().message('Name is required')  // custom error messages

// Nested
tm.object({
  user: tm.object({
    name: tm.string(),
    email: tm.string().optional(),
  }),
  scores: tm.array(tm.number()),
  status: tm.enum(['active', 'inactive']).default('active'),
})

Type coercion

The schema validator intelligently coerces values when possible:

// String → Number
parse('{"age": "30"}', tm.object({ age: tm.number() }))
// => { age: 30 }

// String → Boolean
parse('{"ok": "true"}', tm.object({ ok: tm.boolean() }))
// => { ok: true }

// Single value → Array
parse('{"tags": "dev"}', tm.object({ tags: tm.array(tm.string()) }))
// => { tags: ["dev"] }

// Number → String
parse('{"id": 123}', tm.object({ id: tm.string() }))
// => { id: "123" }

// Case-insensitive enum
parse('{"mood": "Positive"}', tm.object({ mood: tm.enum(['positive', 'negative']) }))
// => { mood: "positive" }

Streaming

Parse JSON as it streams from an LLM. Works with OpenAI, Anthropic, Google, and any SSE-based API.

JsonStream — Incremental parser

import { JsonStream, tm } from 'typemeld';

const schema = tm.object({
  sentiment: tm.enum(['positive', 'negative', 'neutral']),
  confidence: tm.number(),
});

const stream = new JsonStream(schema);

// Push chunks as they arrive from the LLM
stream.push('{"sentiment":');
stream.push(' "positive",');
stream.push(' "confidence": 0.95}');

console.log(stream.value); // partial result available at any time
const final = stream.complete(); // validates against schema
// => { sentiment: "positive", confidence: 0.95 }

streamParse() — Process async iterables

import { streamParse, tm } from 'typemeld';

// With OpenAI
const completion = await openai.chat.completions.create({
  model: 'gpt-4o-mini',
  stream: true,
  messages: [{ role: 'user', content: 'Analyze this text...' }],
});

const schema = tm.object({
  sentiment: tm.enum(['positive', 'negative', 'neutral']),
  topics: tm.array(tm.string()),
});

for await (const { partial, done } of streamParse(
  (async function*() {
    for await (const chunk of completion) {
      yield chunk.choices[0]?.delta?.content ?? '';
    }
  })(),
  schema
)) {
  console.log(partial); // progressively more complete object
  if (done) console.log('Final validated result:', partial);
}

parseStream() — Simple async completion

import { parseStream, tm } from 'typemeld';

// Buffer all chunks and return final validated result
const result = await parseStream(
  (async function*() {
    for await (const chunk of anthropicStream) {
      yield chunk.delta?.text ?? '';
    }
  })(),
  schema
);

Zod Adapter

Already using Zod? Use your existing schemas with typemeld's repair engine:

import { z } from 'zod';
import { fromZod, parse } from 'typemeld';

// Your existing Zod schema
const zodSchema = z.object({
  name: z.string().min(1),
  age: z.number().min(0).max(150),
  role: z.enum(['admin', 'user', 'guest']),
  tags: z.array(z.string()).optional(),
});

// Convert to typemeld and parse messy LLM output
const tmSchema = fromZod(zodSchema);
const result = parse(messyLlmOutput, tmSchema);
// typemeld repairs the JSON, then validates with your Zod-equivalent schema

The adapter supports: string, number, boolean, array, object, enum, literal, union, optional, nullable, default, describe, min/max, passthrough, strict, and more.

Retry Wrapper

Automatically re-prompt the LLM when validation fails:

import { withRetry, tm } from 'typemeld';
import OpenAI from 'openai';

const openai = new OpenAI();
const schema = tm.object({
  entities: tm.array(tm.object({
    name: tm.string(),
    type: tm.enum(['person', 'org', 'location']),
    confidence: tm.number().min(0).max(1),
  })).min(1),
});

const result = await withRetry({
  schema,
  maxRetries: 3,
  call: (messages) => openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages,
  }),
  extract: (res) => res.choices[0].message.content,
  prompt: 'Extract all entities from: "Tim Cook announced Apple\'s new product in Cupertino"',
  onRetry: (attempt, errors) => console.log(`Retry ${attempt}:`, errors),
});
// If the LLM returns invalid output, typemeld re-prompts with error details
// => { entities: [{ name: "Tim Cook", type: "person", confidence: 0.98 }, ...] }

With Anthropic

import Anthropic from '@anthropic-ai/sdk';
import { withRetry, tm } from 'typemeld';

const anthropic = new Anthropic();

const result = await withRetry({
  schema: tm.object({
    summary: tm.string().min(10).max(200),
    keywords: tm.array(tm.string()).min(3),
    sentiment: tm.enum(['positive', 'negative', 'neutral']),
  }),
  call: (messages) => anthropic.messages.create({
    model: 'claude-sonnet-4-20250514',
    max_tokens: 1024,
    system: messages.find(m => m.role === 'system')?.content,
    messages: messages.filter(m => m.role !== 'system'),
  }),
  extract: (res) => res.content[0].text,
  prompt: `Summarize and analyze this article: "${articleText}"`,
});

Real-world examples

Sentiment analysis

const sentiment = parse(llmOutput, tm.object({
  sentiment: tm.enum(['positive', 'negative', 'neutral', 'mixed']),
  confidence: tm.number().min(0).max(1),
  reasoning: tm.string().optional(),
}));

Entity extraction

const entities = parse(llmOutput, tm.object({
  entities: tm.array(tm.object({
    text: tm.string(),
    type: tm.enum(['person', 'organization', 'location', 'date', 'money']),
    start: tm.number().optional(),
    end: tm.number().optional(),
  })),
}));

Product listing

const product = parse(llmOutput, tm.object({
  name: tm.string().min(1),
  price: tm.number().min(0),
  currency: tm.enum(['USD', 'EUR', 'GBP']).default('USD'),
  inStock: tm.boolean().default(true),
  tags: tm.array(tm.string()),
  description: tm.string().max(500),
}));

Code review

const review = parse(llmOutput, tm.object({
  issues: tm.array(tm.object({
    severity: tm.enum(['critical', 'warning', 'info']),
    line: tm.number().optional(),
    message: tm.string(),
    suggestion: tm.string().optional(),
  })),
  overall: tm.enum(['approve', 'request_changes', 'comment']),
  summary: tm.string(),
}));

Translation

const translation = parse(llmOutput, tm.object({
  original: tm.string(),
  translated: tm.string(),
  language: tm.string(),
  confidence: tm.number().min(0).max(1),
  alternatives: tm.array(tm.string()).optional(),
}));

Classification

const classification = parse(llmOutput, tm.object({
  category: tm.enum(['bug', 'feature', 'question', 'docs']),
  priority: tm.enum(['low', 'medium', 'high', 'critical']),
  labels: tm.array(tm.string()),
  assignee: tm.string().optional(),
}));

Structured data extraction from documents

const invoice = parse(llmOutput, tm.object({
  vendor: tm.string(),
  invoiceNumber: tm.string(),
  date: tm.string().transform(s => new Date(s)),
  items: tm.array(tm.object({
    description: tm.string(),
    quantity: tm.number().min(1),
    unitPrice: tm.number().min(0),
  })),
  total: tm.number().min(0),
  currency: tm.enum(['USD', 'EUR', 'GBP', 'JPY']).default('USD'),
}));

Multi-step agent output

const agentStep = parse(llmOutput, tm.object({
  thought: tm.string(),
  action: tm.enum(['search', 'calculate', 'respond', 'ask_user']),
  actionInput: tm.string().optional(),
  observation: tm.string().optional(),
  finalAnswer: tm.string().optional(),
}).passthrough()); // keep any extra fields the LLM adds

With transforms and refinements

const userProfile = parse(llmOutput, tm.object({
  email: tm.string()
    .transform(s => s.trim().toLowerCase())
    .refine(s => s.includes('@'), 'Must be a valid email'),
  age: tm.number()
    .min(0).max(150)
    .refine(n => Number.isInteger(n), 'Age must be a whole number'),
  bio: tm.string()
    .transform(s => s.trim())
    .refine(s => s.length > 0, 'Bio cannot be empty'),
  website: tm.string().optional()
    .transform(s => s?.startsWith('http') ? s : `https://${s}`),
}));

With preprocessing

const config = parse(llmOutput, tm.object({
  temperature: tm.number()
    .preprocess(v => typeof v === 'string' ? parseFloat(v) : v)
    .min(0).max(2),
  model: tm.string()
    .preprocess(v => typeof v === 'string' ? v.trim() : String(v)),
}));

Why not just use zod?

zod is excellent for general validation. typemeld is purpose-built for LLM outputs:

| Feature | zod | typemeld | |---|---|---| | JSON repair (fences, commas, quotes) | ❌ | ✅ | | Extract JSON from prose | ❌ | ✅ | | Fix truncated JSON | ❌ | ✅ | | Streaming JSON parser | ❌ | ✅ | | LLM retry wrapper | ❌ | ✅ | | Zod schema adapter | N/A | ✅ | | Smart type coercion | Partial | ✅ Full | | Single value → array coercion | ❌ | ✅ | | Case-insensitive enum matching | ❌ | ✅ | | Multiple JSON extraction | ❌ | ✅ | | LLM prompt generation | ❌ | ✅ | | Transform / refine / preprocess | ✅ | ✅ | | Custom error messages | ✅ | ✅ | | Min/max constraints | ✅ | ✅ | | passthrough / strict modes | ✅ | ✅ | | TypeScript types | ✅ Built-in | ✅ Built-in | | Zero dependencies | ❌ (standalone) | ✅ | | Bundle size | ~14KB | ~3KB |

Use zod for form validation. Use typemeld for LLM outputs. Or use both together with fromZod().

TypeScript

typemeld ships with built-in TypeScript declarations. Full autocomplete and type inference out of the box:

import { parse, safeParse, tm } from 'typemeld';
import type { Infer, SafeParseResult } from 'typemeld';

const userSchema = tm.object({
  name: tm.string(),
  age: tm.number().optional(),
  roles: tm.array(tm.enum(['admin', 'user'])),
});

type User = Infer<typeof userSchema>;
// { name: string; age?: number; roles: ('admin' | 'user')[] }

const result: SafeParseResult<User> = safeParse(llmOutput, userSchema);

Contributing

Contributions welcome! High-impact areas:

  • More LLM output edge cases and repair strategies
  • Integration guides for popular frameworks (LangChain, Vercel AI SDK, AutoGPT)
  • XML/YAML repair modes
  • Performance benchmarks
git clone https://github.com/suhashollakc/typemeld.git
cd typemeld && npm install && npm test

License

MIT © Suhas Holla