JSPM

typemeld

1.0.1
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 10
  • Score
    100M100P100Q33643F
  • License MIT

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

Package Exports

  • typemeld
  • typemeld/repair
  • typemeld/schema

Readme

npm version license stars dependencies size

typemeld

Parse, validate, and repair messy LLM outputs into clean, typed data.

Like zod, but for AI. Zero dependencies. TypeScript-ready. ~3KB gzipped.


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

// 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" }

Real-world example

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

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

const client = new Anthropic();
const response = await client.messages.create({
  model: 'claude-sonnet-4-20250514',
  max_tokens: 1024,
  system: `You are a text analyzer.\n${promptFor(schema, { strict: true })}`,
  messages: [{ role: 'user', content: `Analyze: "${articleText}"` }],
});

// This just works — even if Claude wraps it in fences or adds commentary
const analysis = parse(response.content[0].text, schema);
console.log(analysis.sentiment);  // "positive"
console.log(analysis.confidence); // 0.92
console.log(analysis.topics);     // ["technology", "innovation"]

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
Smart type coercion Partial ✅ Full
Single value → array coercion
Case-insensitive enum matching
Multiple JSON extraction
LLM prompt generation
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.

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
  • Streaming JSON support (parse partial chunks)
  • zod adapter (tm.fromZod(zodSchema))
  • Retry wrapper (re-prompt LLM on validation failure)
  • XML/YAML repair modes
git clone https://github.com/suhashollakc/typemeld.git
cd typemeld && npm install && npm test

License

MIT © Suhas Holla