@flattype/vue

Vue 3 composables for FlatType.js: fetch Flat Notation or JSON into typed models, with Suspense and streaming rows.
2,180 2
Install
npm install @flattype/vue
Latest Version:1.14.0
Vue:3
License:MIT
Last Updated:Oct 17, 2026
Links: GitHub  ·  npm
Maintainer: karammm

FlatType.js

Flatten API JSON. Get typed models. Save space and tokens.
A tiny TypeScript toolkit that turns backend responses into compact key: value text and maps them to typed models, for any frontend framework and for LLMs such as Claude.

npm @flattype/core npm @flattype/models npm @flattype/http npm @flattype/react npm @flattype/vue npm @flattype/angular npm @flattype/svelte npm @flattype/ai npm @flattype/cli CI 6.2 KB min+gz No third-party dependencies MIT license

Documentation · Playground · Quick start · Format · Spec · Models · HTTP · React · Vue · Angular · Svelte & Solid · Claude & LLMs · CLI · Benchmarks · Changelog · Roadmap

FlatType in 92 seconds: an API response encoded to Flat Notation, how tables and dot paths keep it small, typed models mapped from snake_case JSON, an Express server answering in Flat Notation or JSON, React, Vue, Angular and Svelte in real apps, the CLI generating models from OpenAPI, a FastAPI app answering in Flat Notation, and the size comparison with JSON and TOON
▶ Watch the 92-second intro in full quality. Every result shown is real output, generated by scripts/video/record.mjs.


Quick start

npm install @flattype/core @flattype/models @flattype/http
import { encode, decode } from "@flattype/core";

encode({ name: "John", age: 30, city: "New York" });
// name: John
// age: 30
// city: New York

decode("name: John\nage: 30"); // { name: "John", age: 30 }

No bundler? One script tag puts everything on a FlatType global:

<script src="https://cdn.jsdelivr.net/npm/@flattype/core@1"></script>
<script>
  FlatType.encode({ name: "John" }); // "name: John"
</script>

Lists, the shape of most API responses, become tables. Keys are written once, and nested objects become dot paths:

encode({
  page: 1,
  users: [
    { id: 1, name: "Alice", address: { city: "Paris" } },
    { id: 2, name: "Bob", address: { city: "Delhi" } },
  ],
});
// page: 1
// users[2]{id,name,address.city}:
//   1, Alice, Paris
//   2, Bob, Delhi

Map the response to typed models in one line:

import { defineModel, type Infer } from "@flattype/models";

const User = defineModel({ id: "number", fullName: "string", createdAt: "date" });
type User = Infer<typeof User>; // { id: number; fullName: string; createdAt: Date }

const users = User.parseList(await res.json()); // full_name → fullName, ISO string → Date

Or let both ends speak Flat Notation, with JSON for every other client:

import { flatExpress, flatFetch } from "@flattype/http";

app.use(flatExpress()); // server: res.json() answers in Flat Notation when the client asks

const users = await flatFetch("/api/users", { model: User, list: true }); // client: User[]

And in React, Vue, Angular, Svelte or Solid:

import { useFlat } from "@flattype/react"; // or "@flattype/vue": the same API, with refs

const { data: users, loading, error } = useFlat("/api/users", { model: User, list: true }); // User[] | undefined

// Angular: signals on top of HttpClient
users = flatResource(() => "/api/users", { model: User, list: true }); // users.value(): User[] | undefined

// Svelte: a store, $users.data; Solid: a resource
const users = flatStore("/api/users", { model: User, list: true });
const [users] = createFlatResource(() => "/api/users", { model: User, list: true });

Why FlatType

  • About half the size on list payloads. A list of 1,000 users is 45% smaller than minified JSON and 50% smaller than TOON, because keys are written once and nested fields are flattened into the table header.
  • Made for LLM context. You pay for every character you send to Claude or another model. Flat Notation is plain key: value text that both models and people read easily, and decode(text, { strict: false }) recovers data from model replies, with a warning for every line it had to repair.
  • Typed models without boilerplate. Declare the fields you want and get full TypeScript types. snake_case keys, numeric strings, ISO dates, defaults and nullable fields are handled for you, and every problem is reported with its path.
  • Drop-in for your API. One middleware for Express, Fastify, Hono or any fetch-API server. Clients that ask get Flat Notation, everyone else still gets JSON, and big lists can be streamed row by row.
  • Lossless. decode(encode(x)) always equals JSON.parse(JSON.stringify(x)). Thousands of random values check this on every test run.
  • Small and safe. The core is 6.2 KB min+gz, with no third-party dependencies. It's ESM + CJS with full types, works in the browser and Node 18+, and guards against __proto__ pollution.

When JSON is the better choice

  • One small object per request. Gzip already removes most of JSON's overhead on the wire, so a three-field object saves only a few bytes. The savings come from lists.
  • Data that only machines read and you don't control both ends. Every JSON tool understands JSON. Flat Notation pays off where you own the client and server, or where the text goes to an LLM.
  • Mixing the two formats in one value. Decode Flat Notation with decode and JSON with JSON.parse; a JSON string inside a Flat Notation value is just a string.

Packages

Package What it does Size (min+gz)
@flattype/core encode, decode, createDecoder, flatten, unflatten, stats, and a <script> build for CDNs 6.2 KB
@flattype/models defineModel, infer, toSource: typed models from JSON or Flat Notation +3.2 KB
@flattype/http flatFetch, createClient, flatAxios, streamRows, and flatExpress / flatFastify / flatHono +2.3 KB
@flattype/react useFlat, useFlatSuspense, useFlatStream, useModel, FlatProvider +1.4 KB
@flattype/vue The same composables for Vue 3, with reactive URLs, plus the createFlat() plugin +1.2 KB
@flattype/angular flatInterceptor for HttpClient, the signal-based flatResource / flatStream, the flatModel pipe +1.8 KB
@flattype/svelte Svelte stores (flatStore, flatStreamStore, modelStore), and /solid with createFlatResource / createFlatStream +1.1 KB / +0.9 KB
@flattype/ai toPrompt, fromModelOutput, countTokens, and MCP / Claude tool results +1.3 KB
@flattype/cli The flattype command: encode, decode, stats, and gen (models from samples or OpenAPI) Node tool
@flattype/shared Shared TypeScript types types only
flattype on PyPI The same encode / decode for Python, FlatMiddleware for FastAPI and any ASGI app, and the LLM helpers Python 3.9+

Everything is documented at karammm.github.io/flattype/docs, and you can try the format on your own data in the playground. See the roadmap.

The format

Flat Notation is line-based. Each line is a path and a value:

name: John
zip: "10001"
user.address.city: Paris
"first name": John
tags[]: admin, editor
empty: {}
none: []
users[2]{id,name,phone?}:
  1, Alice, 555-0100
  2, Bob,
events[+].type: push
.commits[1]{sha}:
  a1f
events[+].type: star
.user: ann
Rule Example
Nested objects become dot paths user.address.city: Paris
Arrays of primitives go inline tags[]: admin, editor
Arrays of objects become a table: count, fields, then one row per line users[2]{id,name}:
A field that some objects lack is marked ?, and its cell is left empty where the key is missing {id,name,phone?}
Other arrays are written item by item: [+] starts the next item, and lines starting with . continue it events[+].type: push
null, true, false and numbers are written as in JSON age: 30
Strings are bare unless they could be misread, then JSON-quoted zip: "10001", note: ""
Keys that aren't [A-Za-z0-9_$-]+ are quoted "first name": John
Lines starting with # are comments # generated 2026-10-16

Other delimiters are written into the brackets, as tags[|]: and users[2|]{id|name}:, so decode needs no options.

Shared objects and cycles

JSON can't describe the same object in two places, or an object that contains itself. With refs: true, FlatType can: the object is written once, and path = first.path everywhere else (~ is the root).

const alice = { id: 1, name: "Alice" };
const team = { lead: alice, members: [alice, { id: 2, name: "Bob" }] };
alice.team = team; // a cycle

const text = encode(team, { refs: true });
// lead.id: 1
// lead.name: Alice
// lead.team = ~
// members[+] = lead
// members[+].id: 2
// .name: Bob

const back = decode(text, { refs: true });
back.members[0] === back.lead; // true: the same object, not a copy
back.lead.team === back; // true

References are off by default on both ends. Without refs, shared objects are written out in full each time and a cycle throws, exactly like JSON.stringify. The decoder rejects reference lines unless you pass refs: true, because a few such lines can describe a graph that is endless (a cycle) or enormous when another tool walks it. Only turn it on for text you trust, read by code that expects shared objects.

The same hooks as JSON

encode takes a replacer and decode takes a reviver, with the same signatures and call order as JSON.stringify and JSON.parse, so existing code moves over unchanged:

encode(user, { replacer: (key, value) => (key === "password" ? undefined : value) });
encode(user, { replacer: ["id", "name"] }); // keep only these keys

const order = decode(text, {
  reviver: (key, value) => (key.endsWith("At") ? new Date(value) : value),
});

For typed results with validation, @flattype/models is usually the better fit.

Big integers, dates, bytes and other types

JSON has no type for a 64-bit ID, a date or a byte array. MessagePack solves this with binary extension types. Flat Notation keeps the text plain and solves it at the two ends instead.

Big integers are exact. A BigInt is written as its digits, and decode(text, { bigint: true }) reads any integer beyond 2⁵³ back as a BigInt instead of rounding it the way JSON.parse does. Python reads them exactly with no option. So a 64-bit ID from a Python, Go or Java backend arrives intact:

encode({ id: 18446744073709551617n });           // id: 18446744073709551617
decode("id: 18446744073709551617");               // { id: 18446744073709552000 }  rounded, like JSON
decode("id: 18446744073709551617", { bigint: true }); // { id: 18446744073709551617n }

Smaller integers stay numbers. Models do this automatically (a "bigint" field is always a BigInt), and so do flatFetch and the CLI.

Other types come from @flattype/core/types, a replacer and a reviver that work with encode and decode as well as with JSON.stringify and JSON.parse:

import { parseJson, replaceTypes, reviveDates, stringifyJson } from "@flattype/core/types";

encode({ tags: new Set(["a", "b"]), avatar: new Uint8Array([104, 105]), seen: new Map([["alice", 1]]) }, { replacer: replaceTypes });
// tags[]: a, b
// avatar: aGk=
// seen.alice: 1

decode("at: 2026-01-01T09:00:00.000Z", { reviver: reviveDates }); // { at: Date }
Helper What it does
replaceTypes Map → object (or [key, value] pairs), Set → array, Uint8Array / Buffer / ArrayBuffer → base64, other typed arrays → arrays, Error → { name, message }
reviveDates Date-time strings with a zone (…Z, …+01:00, Python's microseconds too) → Date. Date-only strings stay strings.
parseJson JSON.parse that keeps big integers exact (Node 21+, Chrome 114+)
stringifyJson JSON.stringify that writes BigInts as digits instead of throwing

Python has the same helpers: encode(x, default=flattype.types.default) handles datetime (written exactly as JavaScript writes dates), bytes, set, Decimal, UUID, enums and dataclasses, and flattype.types.revive_dates turns the strings back into datetimes.

Broken input: where the data is wrong

decode is strict by default: the first problem throws a FlatDecodeError with its line and column. With strict: false it repairs what it can instead, and onWarning tells you what it repaired and where:

const people = decode(text, {
  strict: false,
  onWarning: (w) => console.warn(`line ${w.line}, column ${w.column}: ${w.message}`),
});
// line 77, column 1: Expected 5 values, got 4: the missing values are null

Each row of a table is one line, so a slip stays in its line. A missing separator in one record of a 197-record file leaves the other 196 exactly right, and the warning names the record to check. A JSON file with one missing colon can't be read at all; see the Edge demo benchmark.

Problem Lenient repair (the warning says so)
A row with too few or too many values Missing values are null; extra ones are dropped
A missing value (a:) null
A duplicate key or index The last value is kept
A skipped index (a[2] before a[0]) The gap is filled with null, up to 1,000 entries
Fewer table rows than declared The table has the rows that are there
A table header without its colon Read as a table anyway
A broken quoted string Kept as text
With skipInvalidLines: true, a line that isn't Flat Notation The line is skipped

flattype decode --lenient file.flat prints the same warnings, as warning: file.flat:77:1: ….

Specification and other languages

Flat Notation 1.1 is a complete specification, with the grammar, every error message, and the exact text an encoder writes. Any language can implement it. The conformance suite tests an implementation through a small adapter, so a port can show that it reads and writes exactly what this one does:

$ node conformance/run.mjs -- python3 my_adapter.py
  Decoder               2615 / 2615   ✓
  Encoder               1944 / 1944   ✓
  ...
Language Package Conformance
JavaScript, TypeScript @flattype/core every level
Python flattype every level

To build one for Go, Rust, Java or another language, start with writing a port, then get it listed.

1.1 and older readers. Since v1.14, encode writes Flat Notation 1.1, which adds optional table fields and [+] items. Every 1.0 document still means the same, but a decoder older than 1.14 can't read the new syntax. For such readers, encode(value, { target: "1.0" }) (and --target 1.0 in the CLI) writes exactly the text older versions wrote. The HTTP servers choose for you (see HTTP).

Typed models

import { defineModel, infer, toSource, type Infer } from "@flattype/models";

const Address = defineModel({ city: "string", zip: "string" });

const User = defineModel({
  id: "number",                                  // "7" is coerced to 7
  name: "string",
  createdAt: "date",                             // reads created_at; ISO string or epoch ms → Date
  bio: "string?",                                // string | null, null when missing
  role: { type: "string", default: "viewer" },
  nickname: { type: "string", optional: true },  // left out when missing
  address: Address,                              // nested model
  tags: ["string"],                              // array
  orderId: "bigint",                             // exact 64-bit IDs (BigInt)
  avatar: "bytes?",                              // base64 → Uint8Array
});
type User = Infer<typeof User>;

User.parse(json);          // a JSON value or a Flat Notation string
User.parseList(json.data); // arrays, including Flat Notation tables
User.safeParse(json);      // { ok: true, value } | { ok: false, error }
User.serialize(user);      // back to API JSON (dates as ISO strings, bytes as base64)
User.encode(users);        // straight to Flat Notation

parse throws a ModelError listing every problem with its path, for example address.zip: Required and tags[1]: Expected string, got object. Unknown keys such as password_hash are dropped.

Option Default Effect
keys "auto" "auto" reads createdAt or created_at. "snake" also serializes back to snake_case. "exact" only reads the field name.
coerce true Converts "42" → 42 and "true"/1 → true. Set false to require exact types.

Generate models from a real response:

console.log(toSource(infer(await res.json()), "UsersPage"));
import { defineModel, type Infer } from "@flattype/models";

export const UsersPageData = defineModel({
  id: "number",
  fullName: "string",
  createdAt: "date",
  bio: "string?",
  vip: { type: "boolean", optional: true },
}, { keys: "snake" });
export type UsersPageData = Infer<typeof UsersPageData>;

export const UsersPage = defineModel({
  page: "number",
  data: [UsersPageData],
});
export type UsersPage = Infer<typeof UsersPage>;

HTTP

@flattype/http adds Flat Notation to an existing API without breaking anyone. A response is re-encoded only when the request says Accept: text/x-flat, and Vary: Accept keeps caches correct.

Server

import { flatExpress, flatFastify, flatHono, negotiate } from "@flattype/http";

expressApp.use(flatExpress());                         // Express: res.json(...)
await fastifyApp.register(flatFastify);                // Fastify: object replies
honoApp.use(flatHono());                               // Hono: c.json(...)
return negotiate(request, Response.json(data));        // Bun, Deno, Next.js route handlers, Workers

Client

import { createClient, flatAxios, flatFetch, streamRows } from "@flattype/http";

const user = await flatFetch("/api/users/7", { model: User });          // typed, throws HttpError on 4xx/5xx

const api = createClient({ baseURL: "https://api.example.com/v1", headers: { authorization } });
const page = await api.get("/users", { query: { page: 2 } });
await api.post("/users", { name: "Chen" });                              // JSON request body

const axiosApi = flatAxios(axios.create({ baseURL: "/api" }));           // axios: response.data is decoded

for await (const { path, index, value } of streamRows("/api/export")) {  // rows while still downloading
  table.append(value);
}

A JSON-only server works with every client helper: the response is just read as JSON.

Mixed versions. Clients from 1.14 send Accept: text/x-flat; version=1.1, and the servers write Flat Notation 1.1 only for them. A client on an older version asks without a version and gets 1.0 text, which it can read, so the server and its clients can be upgraded in any order.

React

npm install @flattype/react @flattype/models
import { FlatProvider, useFlat, useFlatStream, useFlatSuspense } from "@flattype/react";

<FlatProvider baseURL="/api" headers={{ authorization: `Bearer ${token}` }}>
  <App />
</FlatProvider>;

function Users() {
  const { data, error, loading, refetch } = useFlat("/users", { model: User, list: true });
  if (error) return <ErrorMessage error={error} />;   // HttpError, ModelError or a network error
  if (!data) return <Spinner />;
  return <UserList users={data} onRefresh={refetch} />; // data: User[]
}

function Profile({ id }: { id: number }) {
  const { data: user } = useFlatSuspense(`/users/${id}`, { model: User }); // inside <Suspense>
  return <h2>{user.fullName}</h2>;                                          // user: User, never undefined
}

function Export() {
  const { rows, done } = useFlatStream("/export", { model: Row, table: "rows" });
  return <Table rows={rows} footer={done ? "Done" : `Loading… ${rows.length}`} />; // rows appear while downloading
}
Hook Returns Notes
useFlat(url, options) { data, error, loading, refetch } A new URL aborts the old request. Old data stays while the next loads. url: null or enabled: false skips the request
useFlatSuspense(url, options) { data, refetch } Suspends, sends errors to an error boundary, and caches by URL (one request for many components)
useFlatStream(url, options) { rows, done, error, restart } One render per network chunk. table picks one table by path
useModel(data, Model, { list }) the mapped value Memoized. For data from props, stores or sockets

Works with React 18 and 19, in StrictMode and with any server. A JSON-only API works too. A runnable app with Playwright tests in three browsers is in examples/react-app.

Vue

npm install @flattype/vue @flattype/models
// main.ts
app.use(createFlat({ baseURL: "/api", headers: () => ({ authorization: `Bearer ${token.value}` }) }));
<script setup lang="ts">
import { useFlat, useFlatStream, useFlatSuspense } from "@flattype/vue";

const props = defineProps<{ id: number }>();

// The URL can be a ref or a getter: changing it aborts the old request and loads the new one.
const { data: user, error, loading, refetch } = useFlat(() => `/users/${props.id}`, { model: User }); // Ref<User | undefined>

// Inside <Suspense>, `await` the first response so `data` is always set.
const { data: me } = await useFlatSuspense("/me", { model: User });                                  // Ref<User>

// Big lists render while they download.
const { rows, done } = useFlatStream("/export", { model: Row, table: "rows" });
</script>
Composable Returns Notes
useFlat(url, options) { data, error, loading, refetch } refs url and enabled can be refs or getters. Aborts on change and on unmount
useFlatSuspense(url, options) Promise<{ data, error, loading, refetch }> For async setup() in <Suspense>. The first error goes to onErrorCaptured
useFlatStream(url, options) { rows, done, error, restart } refs One update per network chunk. table picks one table by path
useModel(source, Model, { list }) a computed ref For props, Pinia stores and sockets

Vue 3.3+. A runnable app with Playwright tests in three browsers is in examples/vue-app.

Angular

npm install @flattype/angular @flattype/models
// main.ts
bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(withInterceptors([flatInterceptor])), // every HttpClient call can read Flat Notation
    provideFlatType({ baseURL: "/api" }),
  ],
});
@Component({
  imports: [FlatModelPipe],
  template: `
    @for (u of users.value()?.data; track u.id) { <li>{{ u.fullName }}</li> }
    <p>{{ (raw() | flatModel: Address)?.city }}</p>
    <p>{{ events.done() ? "Done" : "Loading… " + events.rows().length }}</p>
  `,
})
export class UsersComponent {
  page = signal(1);
  users = flatResource(() => `/users?page=${this.page()}`, { model: UsersPage }); // refetches when page changes
  events = flatStream(() => "/export", { model: Row, table: "rows" });           // rows while downloading
  raw = toSignal(inject(HttpClient).get("/api/address"));                        // plain HttpClient works too
}
API What it does
flatInterceptor Adds Accept: text/x-flat, application/json and decodes either format, error bodies included. Skip a request with FLAT_TYPE_SKIP in its HttpContext
flatResource(() => url, options) value, error and isLoading signals plus reload(). Runs on HttpClient and cancels the old request when the URL changes
flatStream(() => url, options) rows, done and error signals plus restart(). Uses fetch, because HttpClient can't stream
flatModel pipe, mapModel / mapModelList Model mapping in templates and in RxJS pipelines

Angular 19+ (built with partial compilation, so it works with every later version). A runnable Angular 22 app with Playwright tests in three browsers is in examples/angular-app.

Svelte and Solid

npm install @flattype/svelte @flattype/models

Svelte (3, 4 and 5). The stores follow the Svelte store contract, so $store just works:

<script lang="ts">
  import { createFlat } from "@flattype/svelte";
  import { derived, writable } from "svelte/store";

  const api = createFlat({ baseURL: "/api", headers: () => ({ authorization: `Bearer ${token}` }) });

  const users = api.store("/users", { model: User, list: true });            // $users.data: User[] | undefined
  const id = writable(1);
  const user = api.store(derived(id, (i) => `/users/${i}`), { model: User }); // the URL can be a store
  const events = api.streamStore("/export", { model: Row, table: "rows" });   // rows while downloading
</script>

{#each $users.data ?? [] as u (u.id)}<li>{u.fullName}</li>{/each}
<p>{$events.done ? "Done" : `Loading… ${$events.rows.length}`}</p>

Solid (@flattype/svelte/solid):

import { createFlatResource, createFlatStream } from "@flattype/svelte/solid";

const [user, { refetch }] = createFlatResource(() => `/users/${props.id}`, { baseURL: "/api", model: User });
const events = createFlatStream(() => "/api/export", { model: Row, table: "rows" });

<Suspense fallback={<Spinner />}>
  <Show when={user()}>{(u) => <h2>{u().fullName}</h2>}</Show>
</Suspense>;
API What it does
flatStore(url, options) A store of { data, error, loading } plus refetch(). Lazy: it fetches on the first subscriber and aborts when the last one leaves. A URL store change aborts the old request
flatStreamStore(url, options) A store of { rows, done, error } plus restart(). One update per network chunk
modelStore(source, Model) Maps another store's value to a model
createFlatResource(() => url, options) A Solid resource, for <Suspense> and <ErrorBoundary>. Aborts on change and on cleanup
createFlatStream(() => url, options) rows, done and error accessors plus restart()

The stores also work in Solid, or anywhere else, through the store contract: from(flatStore(...)). Runnable apps with Playwright tests in three browsers are in examples/svelte-app and examples/solid-app.

Claude and LLMs

npm install @flattype/ai
import Anthropic from "@anthropic-ai/sdk";
import { countTokens, fromModelOutput, outputInstructions, toPrompt } from "@flattype/ai";

const client = new Anthropic();

// 1. Data in: Flat Notation inside <data> tags, with a one-line legend the model reads once.
const response = await client.beta.messages.create({
  model: "claude-opus-5-5",
  max_tokens: 16000,
  betas: ["server-side-fallback-2026-07-01"],
  fallbacks: "default", // if a safety classifier declines, retry on the recommended fallback model
  messages: [{
    role: "user",
    content: `${toPrompt(orders, { name: "orders" })}\n\nTotal spent per customer?\n\n${outputInstructions([{ customer: "Alice", spent: 120 }])}`,
  }],
});

// 2. Data out: tolerant of tags, fences, prose and JSON, then validated into typed models.
const reply = response.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
const totals = fromModelOutput(reply, { model: Total, list: true }); // Total[]

// 3. Measure it with Claude's own tokenizer (count_tokens is free).
const { json, flat, saved } = await countTokens(client, orders);

Tool results shrink too, which matters most for agents that call tools over and over:

import { claudeToolResult, toolResult } from "@flattype/ai";

// MCP server (@modelcontextprotocol/sdk): returns a CallToolResult with Flat Notation text
server.registerTool("list_orders", { description: "…" }, async () => toolResult(await db.orders()));

// Claude API tool loop: a tool_result block for the next user message
messages.push({ role: "user", content: [claudeToolResult(toolUse.id, await runTool(toolUse))] });
API What it does
toPrompt(data, { name, legend }) <data format="flat-notation"> plus a one-line legend. The block is still valid Flat Notation; put the legend in your system prompt once with legend: false
outputInstructions(example) Asks the model to answer in Flat Notation and shows the exact shape you expect
fromModelOutput(reply, { model, list, onWarning }) Finds the data (last <data> block, code fence, or the text itself), skips prose lines, and accepts JSON too. Repairs go to onWarning, with their line. JSON that doesn't parse throws ModelOutputError, saying where it breaks: The data is JSON, but it is broken at line 1, column 22261: …"language" ⟶ "Galician"…
countTokens(client, data, { model }) { json, flat, saved } in real Claude tokens, minus per-message overhead. Takes any client with messages.countTokens
toolResult(data, { structured }) / claudeToolResult(id, data) MCP CallToolResult and Claude API tool_result blocks

See examples/ai-claude for a token report, a full Claude round trip, and a runnable MCP server (claude mcp add flattype-orders -- node mcp-server.mjs).

CLI

npx @flattype/cli --help        # or: npm install -D @flattype/cli, then use `flattype …`
# Convert, in pipes or files
curl -s https://api.example.com/users | npx @flattype/cli encode
npx @flattype/cli decode users.flat -o users.json

# Compare sizes
npx @flattype/cli stats users.json
#                 bytes      gzip
# JSON           169545     12890
# Flat            93606     12568
# saved             45%        2%

# Generate typed models from an OpenAPI spec (3.0, 3.1 or Swagger 2; JSON or YAML)…
npx @flattype/cli gen --openapi openapi.yaml -o src/models.ts

# …or from a real response
curl -s https://api.example.com/users > users.json && npx @flattype/cli gen --sample users.json --name UsersPage

For an OpenAPI schema like

Customer:
  type: object
  required: [id, full_name, created_at]
  properties:
    id: { type: integer }
    full_name: { type: string }
    created_at: { type: string, format: date-time }
    address: { $ref: "#/components/schemas/Address" }

flattype gen --openapi writes:

export const Customer = defineModel({
  id: "integer",
  fullName: "string",
  createdAt: "date",
  address: { type: Address, optional: true },
}, { keys: "snake" });
export type Customer = Infer<typeof Customer>;

It handles $ref between schemas, allOf, required (other fields become optional), nullable and type: [..., "null"], date and date-time formats, enums and inline objects. A recursive reference becomes "any", and the CLI warns you. Exit code 1 means bad input, 2 means a usage error. From code, use fromOpenAPI(spec) together with toSource(models).

Python

pip install flattype gives Python backends the same format, byte for byte. It has no dependencies and supports Python 3.9+. Thousands of shared test cases check that both languages write and read exactly the same text, errors included.

import flattype
from fastapi import FastAPI
from flattype.asgi import FlatMiddleware

flattype.encode({"name": "John", "age": 30})  # "name: John\nage: 30"
flattype.decode("name: John\nage: 30")  # {'name': 'John', 'age': 30}

app = FastAPI()
app.add_middleware(FlatMiddleware)  # JSON responses answer in Flat Notation when the client asks

So a FastAPI backend and a React frontend using useFlat talk Flat Notation with one line on each side. flattype.ai has to_prompt, from_model_output and count_tokens for Claude. See the Python README and the FastAPI example.

Benchmarks

pnpm compare on the fixtures in fixtures/ (Node 24, Apple Silicon):

Payload JSON (minified) TOON Flat Notation vs JSON gzip vs JSON
List of 100 users 16,691 B 18,589 B 9,151 B −45% −6%
List of 1,000 users 169,545 B 188,544 B 93,606 B −45% −2%
1,000 contacts, some keys missing 101,947 B 110,753 B 59,707 B −41% −5%
Activity feed, 500 events of 4 kinds 162,440 B 184,935 B 169,092 B +4% −3%
Mixed-shape events 3,606 B 3,785 B 3,179 B −12% −4%
Single profile object 146 B 133 B 151 B +3% −5%
Decode 1,000 users Time
JSON.parse (native) 0.34 ms
FlatType decode 0.73 ms
TOON decode 5.9 ms

Numbers measured on every docs build are on the benchmarks page. Some honest notes on them:

  • The savings come from lists. A single small object is about the same size as JSON. Lists whose objects have different keys became tables with optional fields (phone?) and [+] items in v1.14: before that, the contacts list was 18% larger than JSON, and 96% larger after gzip.
  • Deeply nested, varied objects are about even with JSON. In the activity feed, each event repeats paths like actor.login where JSON nests them once.
  • Gzip already removes most of JSON's repeated keys, so gzipped network sizes come out close. FlatType makes the biggest difference where text isn't compressed: LLM prompts and tool results, logs, caches and local storage.
  • JSON.parse is native code, so it's still faster. FlatType decodes at about half its speed and about 8× faster than the TOON reference decoder.
  • MessagePack is binary, and bigger than you'd think. It saves 5% before compression on the Edge files below but comes out 11% larger than JSON after gzip, because it repeats every key like JSON does. Flat Notation is text you can read, log and put in a prompt.

Real-world files: Microsoft's Edge demo JSON

Six public files from 64 KB to 5 MB, each a list of people with a long bio text, in both their pretty-printed and -min.json versions:

File .json (pretty) -min.json Flat Notation Saved vs. pretty Saved vs. min
5MB (15,840 records) 4.89 MB 4.43 MB 3.73 MB 24% 16%
1MB (3,168 records) 1002.2 KB 906.4 KB 763.1 KB 24% 16%
64KB (197 records) 62.3 KB 56.3 KB 47.5 KB 24% 16%

Long text leaves fewer keys to remove, so the saving is smaller than on typical API lists. Once a response is gzipped, the two are within 2% of each other. Full report, with every size, compression and timing.

Development

pnpm install
pnpm build && pnpm typecheck && pnpm test
pnpm bench        # encode/decode vs JSON
pnpm compare      # size and parse time vs JSON and TOON
pnpm check:size   # bundle budgets
pnpm --filter @flattype/core test:perf                        # linear-time checks on hostile input
FUZZ_RUNS=50000 pnpm --filter @flattype/core exec vitest run  # deep fuzzing, as in CI
pnpm --filter @flattype/docs dev                              # docs site (VitePress)
pnpm --filter @flattype/playground dev                        # playground
pnpm video        # re-render docs/assets/flattype-intro.{mp4,gif} (needs ffmpeg and the Angular example built)

This is a pnpm + Turborepo monorepo built with tsup and tested with Vitest and fast-check. Releases use Changesets, and all @flattype/* packages share one version number. Every release follows the checklist in ROADMAP.md.

Security

The decoder is prototype-safe, runs in linear time on hostile input, and only ever throws FlatDecodeError. Deep fuzzing and timing tests check this on every push, as described on the security page. Please report vulnerabilities privately, following SECURITY.md.

Contributing

Issues and pull requests are welcome. If you have a payload that FlatType handles badly, please open an issue with a sample; it becomes a test fixture.

License

MIT

Related Packages

gradual-json

Parse async JSON string chunks into progressively constructed objects, including...

491 0
swr-vue

Vue composables for Data fetching

1,877 13
@json-render/vue

Vue renderer for @json-render/core. JSON becomes Vue components.

433,381 18,809
@xmachines/json-render-vue

Vue renderer for @xmachines/json-render-core. JSON becomes Vue components.

9,327
nuxt-openapi-hyperfetch

Nuxt module for generating an OpenAPI SDK, composables, server routes, and headl...

4,669

Version History

Version Released Vue License
1.14.0 >=3.3 MIT
1.13.0 >=3.3 MIT
1.12.0 >=3.3 MIT
1.11.0 >=3.3 MIT
1.10.0 >=3.3 MIT
Pre-releases (1)
Version Released License
0.0.0-stage