A data format for the wire and for humans — JSON's data model, a schema on the first line, and about half the bytes for a collection. Validated as it is parsed, with errors that say what and where.
id: int, name: string, email: email, age: int, active: bool
---
~ 1, Alice Johnson, alice@example.com, 30, T
~ 2, Bob Smith, bob@example.com, 25, F
~ 3, Carol White, carol@example.com, 28, TThe first line is the schema. --- ends the header. Each ~ line is one record — values
only, in schema order, because the names were already said once. Three records above is 190 bytes
against 243 as JSON; at a hundred records it is 48% smaller, and the gap only widens, because
JSON repeats every key on every row and Internet Object never does.
Try it live at play.internetobject.org — paste the block above, break something, and watch the error land on the exact token.
- Smaller on the wire. Keys are written once, in the schema, not once per record. The saving grows with the data.
- Validated as it parses. A wrong type, a missing value, a bad email — reported with a code, a message, and a line and column. Not a separate validation step you forget to run.
- Typed, with the types you actually need.
int,number,decimal,bigint,bool,string,email,url,date,time,datetime,base64, arrays, objects, nullable and optional members, choices, ranges, patterns — in the schema, not in your code. - Readable by a person. Open strings need no quotes, records read like a table, and a document with a header is self-describing.
- A format, not a library. An open specification with a language-independent conformance corpus of 1,500+ cases, so an implementation in another language reads your data the same way this one does.
JSON Internet Object
[ name: string, age: int
{"name": "Alice", "age": 30}, ---
{"name": "Bob", "age": 25} ~ Alice, 30
] ~ Bob, 25
| Install | npm install internet-object |
| Dependencies | none |
| Runtime | Node ≥ 18 and modern browsers |
| Modules | ESM and CommonJS, TypeScript types included |
| Size | ~43 KB min+gzip for the full library, parser + schema + streaming |
| License | Apache 2.0 |
| Spec | docs.internetobject.org · Playground play.internetobject.org |
npm install internet-object # stable (latest)
npm install internet-object@next # preview (next)Each section builds on the previous one. Start at the top and work your way down.
The shortest way to use Internet Object: write it inline. The bare io tag gives you plain
JavaScript.
import io from 'internet-object';
const obj = io`
name: string, age: int
---
Alice, 30
`;
console.log(obj); // { name: 'Alice', age: 30 } ← plain JavaScriptThe line above --- is the schema; the line below it is the data. Prefix each row with ~ and you
get a collection instead:
const people = io`
name: string, age: int
---
~ Alice, 30
~ Bob, 25
`;
// [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }]io.doc reads the same text and gives you the document instead — for the header, sections,
validated writes, or a round trip back to IO text.
const doc = io.doc`
name: string, age: int
---
Alice, 30
`;
doc.data.name; // 'Alice'
doc.data.age = 31; // validated against the schema; a bad value throws
String(doc); // back to IO text
doc.toObject(); // the plain projection — the same thing io`` gives youAn interpolated ${value} is always written as a value, never spliced in as source, so
${'Smith, John'} stays one string and ${'1,000'} stays one thousand.
Tags are ideal for tests, fixtures and prototyping — anywhere the IO is written by you rather than
received. io.schema and io.defs do the same for a schema and a definitions block, and every tag
takes a .with(defs, sink) form:
const person = io.schema`{name: string, age: int}`;
io.with(person)`Alice, 30`; // { name: 'Alice', age: 30 }When the text arrives from a file, an HTTP response or an editor, use the parse functions. Same split, same rule — plain by default, the document when you ask for it by name.
import { parse, parseDocument } from 'internet-object';
const text = `
name: string, age: int
---
Alice, 30
`;
parse(text); // { name: 'Alice', age: 30 } ← plain JavaScript
parseDocument(text); // the documentWhat happened?
- both read the text and validate it against the schema on the first line
parsegives back plain JavaScript — no wrapper, nothing to unwrap- the tags are these functions in template form:
io`…`isparse,io.doc`…`isparseDocument
Values keep their real types: a date is a Date, a decimal a Decimal, a bigint a BigInt.
Call toJSON() when you need the spelling JSON can carry.
parse / io`…` |
parseDocument / io.doc`…` |
|
|---|---|---|
| Returns | plain objects and arrays | the document |
Crosses structuredClone / postMessage / RSC |
✅ | use toObject() |
| Header, sections, round trip to IO text | — | ✅ |
| Validated writes, change notification | — | ✅ |
A document is also a store, with no framework package involved:
import io, { parseDocument } from 'internet-object';
const doc = parseDocument(text);
const stop = io.subscribe(doc, (value) => render(value)); // the Svelte store contract
io.version(doc); // the React snapshot
const useIO = (doc) => useSyncExternalStore(cb => io.subscribe(doc, cb), () => io.version(doc));Add more rows after --- to create a collection:
const text = `
name: string, age: int
---
~ Alice, 30
~ Bob, 25
~ Carol, 28
`;
console.log(parse(text));
// [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }, { name: 'Carol', age: 28 }]The ~ is what makes a collection, not the row count: a lone ~ Alice, 30 is an array of one,
and a bare Alice, 30 with no ~ is a single object. Each ~ row is one record.
Wrap plain JavaScript objects into IO structures (IODocument or IOObject). This is useful for building data programmatically before serialization or validation.
import { load, loadObject, IODocument, IOObject } from 'internet-object';
// 1. Create a full document
const doc = load({ title: 'User List', count: 10 });
console.log(doc instanceof IODocument); // true
// 2. Create a single IOObject
const user = loadObject({ name: 'Alice', active: true });
console.log(user instanceof IOObject); // true
// console.log(user.get('name')); // 'Alice'IO objects support both key-based and positional access:
import { loadObject, parseDefinitions } from 'internet-object';
const defs = parseDefinitions('~ $schema: { name: string, age: int }');
const user = loadObject({ name: 'Alice', age: 30 }, defs);
// By key
console.log(user.get('name')); // 'Alice'
// By position (insertion order)
console.log(user.getAt(0)); // 'Alice'
console.log(user.getAt(1)); // 30For collections:
import { loadCollection, parseDefinitions } from 'internet-object';
const defs = parseDefinitions('~ $schema: { name: string, age: int }');
const users = loadCollection([
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 25 },
], defs);
console.log(users.getAt(0).get('name')); // 'Alice'
console.log(users.getAt(1).get('name')); // 'Bob'Check your existing JavaScript objects against an IO schema.
Method A: load() (Strict)
Throws an error if validation fails. Use this when you need an IODocument instance or expect valid data.
import { load, parseDefinitions } from 'internet-object';
const defs = parseDefinitions('name: string, age: {int, min: 30}');
try {
// changing age to less than 30 will throw an error
const doc = load({ name: 'Alice', age: 30 }, defs);
console.log(doc.toObject());
} catch (e) {
console.error('Validation failed:', e.message);
}Method B: validateObject() (Safe)
Returns { valid, errors } and never throws. Great for form inputs.
import { validateObject, parseDefinitions } from 'internet-object';
const defs = parseDefinitions('~ $schema: { name: string, age: int }');
const result = validateObject({ name: 'Alice', age: 'thirty' }, defs);
if (!result.valid) {
for (const e of result.errors) console.error(e.errorCode, e.message);
// expected-integer Expecting a value of type 'int' for 'age'
}Once you have validated data, you can serialize it back to IO format:
import { load, parseDefinitions, stringify } from 'internet-object';
const defs = parseDefinitions('~ $schema: { name: string, age: int }');
const doc = load({ name: 'Alice', age: 30 }, defs);
console.log(stringify(doc));
// Alice, 30This is the reverse of parse(). Round-trip: parse() → load() → stringify().
Define multiple schemas and reference them by name:
import { load, parseDefinitions } from 'internet-object';
const defs = parseDefinitions(`
~ $address: { street: string, city: string }
~ $user: { name: string, age: int, address: $address }
~ $schema: $user
`);
const doc = load({
name: 'Alice',
age: 30,
address: { street: '123 Main St', city: 'NYC' }
}, defs);
console.log(doc.toObject());Schemas starting with $ are named. $schema is the default schema used for validation.
For large datasets or network streams, use createStreamReader:
import { createStreamReader } from 'internet-object';
const input = getSomeReadStream(); // Fetch Response, ReadableStream, Generator...
const reader = createStreamReader(input);
for await (const item of reader) {
if (item.data) {
console.log('Received:', item.data.toObject());
}
}Works with Node.js streams, WHATWG streams, AsyncIterable, or simple strings.
| I want to… | Use this |
|---|---|
| Parse IO text to JS | parse(text) |
| Parse IO text to a document | parseDocument(text) |
| Validate JS data | load(data, defs) or validateObject(data, defs) |
| Convert JS to IO text | stringify(load(data, defs)) |
| Embed IO in code | io.doc`...` |
| Create a schema | parseDefinitions('~ $schema: {...}') or io.schema`{...}` |
| Read stream | createStreamReader(source) |
More Features
Keep the schema out of the document — say, shared between a server and its clients — and pass it
in. Only $schema is applied to the data; other $names are there to be referenced.
import { parse, parseDefinitions } from 'internet-object';
const defs = parseDefinitions('~ $schema: { name: string, age: int }');
console.log(parse('Alice, 30', defs));
// { name: 'Alice', age: 30 }With nothing extra, the first problem throws — and the complete list rides along as
err.errors, so one run shows everything. When you would rather have the data and the errors,
take safeParse:
const { ok, data, errors } = safeParse(text);
for (const row of data ?? []) {
if (io.isError(row)) continue; // a failed record, embedded in place with its position
use(row);
}It never throws. Each error carries a stable errorCode, a message, and the row and column of the
token; a failed record sits in data exactly where it occurred (or is omitted with
{ skipErrors: true } — it stays in errors either way, so nothing is lost).
safeParseDocument is the same idea with the document under doc.
A sink — an array or a function in the third argument — is the lower-level form of the same
opt-in, useful when you want to route errors somewhere yourself. There is no strict option
because there is nothing left for it to decide.
const errors: Error[] = [];
const data = parse(text, defs, errors);
for (const e of errors) console.error(e.errorCode, e.message);
// invalid-email Invalid email address: not-an-email at 3:13
// expected-integer Expecting a value of type 'int' for 'age' at 4:28import { loadInferred } from 'internet-object';
const doc = loadInferred({ name: 'Alice', age: 30 });
// Schema is auto-generated: { name: string, age: number }Experimental. Inference is a convenience of this library, not part of the Internet Object format — it guesses a schema, where everything else is determined by its input. It carries no compatibility promise and may change in any release, and implementations in other languages need not provide it. For data you intend to keep or exchange, write the schema and use
load().
import { parse, stringifyDocument } from 'internet-object';
const doc = parse(text);
// Include header and section names in output
const output = stringifyDocument(doc, {
includeHeader: true,
includeSectionNames: true
});Core Classes
import {
IODocument, // Full document (header + sections)
IOObject, // Single record
IOCollection, // Array of records
IODefinitions, // Schema definitions
IOSchema, // Compiled schema
IOError, // Base error
IOValidationError, // Validation error
IOSyntaxError // Parse error
} from 'internet-object';Feature Status
- Parsing: ✅
- Schema validation: ✅
- Type system: ✅ (string, int, number, decimal, bigint, bool, email, url, date, time, datetime, base64, arrays, objects, nullable/optional, choices, ranges, patterns)
- Load/validate API: ✅
- Stringify API: ✅
- Error handling: ✅ (codes, messages, positions; accumulate or throw)
- Streaming: ✅
- Schema inference: 🧪 experimental — a convenience of this library, not part of the format
Per-feature stability tiers and the versioning policy live in the Internet Object specification (docs.internetobject.org). The current published implementation version is shown by the npm badge at the top.
yarn install # Install dependencies
yarn test # Run tests
yarn build # Build for productionInternet Object is a format, not a library, so correctness is defined outside this repo. A
language-independent corpus of 1,500+ cases — written in Internet Object itself — is the contract
every implementation must satisfy, in any language, and npm test runs it as part of this suite.
A reference implementation that does not run the contract on every commit is only a second opinion
that happens to be nearby.
The corpus repository is not public yet. It is being finalised and will be released alongside the 1.0 specification. Until then this section is for maintainers who already have it.
Check it out beside this repo and the tooling finds it automatically:
your-workspace/
InternetObject-js/ # this repo
InternetObject-test-cases/ # the corpus
InternetObject-specs/ # the specification
The lookup is by repository name, not by a hardcoded path, so any checkout layout works — set
IO_CORPUS_DIR or IO_SPECS_DIR to point elsewhere. If a sibling is absent the corpus suites
skip rather than fail, so a clone or a tarball build still works; you are then running the
library's own tests only. Cases are generated from the tables in tools/corpus/suites-*.ts — edit
those, never the .io files.
Maintainers: bash scripts/publish-latest.sh for a stable release, bash scripts/publish-next.sh
for a preview on the next tag.
We welcome contributions! Please see our CONTRIBUTING.md for details on how to get started, report issues, or propose new features.
- Issues: Report a bug
- Discussions: Join the conversation
- X: @internetobject
For the full specification, visit docs.internetobject.org.
Apache License 2.0 · © 2018-2026 ManiarTech®
Permissive and corporate-friendly: use it in commercial or closed-source products, with an explicit patent grant. Just retain the copyright and license notice.
Internet Object is a trade name and unregistered trademark of Maniar Technologies. Per Section 6 of the license, the grant covers the code, not the name or the logos.
