json-source
json-source reads a JSON file (a FILE arg, or stdin) whose top-level value is an ARRAY and emits one element per line (JSONL) — the front of a pipe you feed into the fold/filter/transform gifts (json-source users.json | dedup-filter --key id). Object elements are records the consuming gifts read directly; scalar elements are a stream of scalars. Each element is re-serialized to canonical compact JSON (key order preserved). Zero dependencies, runs unchanged in Node or a browser, same input yields byte-identical output every run.
The honest edge
json-source streams the elements of a TOP-LEVEL JSON ARRAY only — a non-array top-level (an object, a number, a string, a boolean, null) or malformed JSON is refused (exit 2) rather than guess how to streamify a single value. Each element is re-serialized to canonical compact JSON, so number tokens and whitespace are normalized (1e3 -> 1000, 1.0 -> 1) though key order is preserved — the JSON value, not the input bytes. It reads JSON, not JSON5/NDJSON, and parses the WHOLE document (not an incremental stream).
Run it
printf '[{"id":1},{"id":2}]' | node json-source.js # -> {"id":1}\n{"id":2}
test_json-source.js (33/33, independent map-route oracle) + Plumb conformance GREEN (21/21, signed 2026-09-12, clock-independent, mutation-bite non-vacuous)
Node / browser, no dependencies
The code — every file that ships
json-source.js171 lineson GitHub →
#!/usr/bin/env node
/* json-source.js — stream the elements of a JSON array as a JSONL record stream.
Dependency-free, deterministic, pure. Runs in Node or a browser. MIT.
WHAT IT IS. A SOURCE: it turns a JSON array — the shape an API response, an export,
or a `[ ... ]` file so often arrives in — into the front of a pipe: one element per
line (JSONL), ready to feed INTO the fold/filter/transform gifts. Give it a file (or
pipe text on stdin) and it emits each top-level array element as one compact JSON line:
[{"id":1},{"id":2}] -> {"id":1}
{"id":2}
[1, 2, 3] -> 1
2
3
When the elements are objects, they are records the consuming gifts read directly:
`json-source users.json | dedup-filter --key id`. When they are scalars, you get a
stream of scalars — still valid JSONL.
TOP-LEVEL ARRAY ONLY (the honesty axis). The input's top-level value MUST be a JSON
array — that is the only thing that IS a stream. A bare object, a number, a string, a
boolean, or null is a single value, not a stream, so json-source REFUSES it (exit 2)
rather than guess how to "streamify" it (wrap it? emit its entries? emit it as one
line?). Each guess is a different tool; refusing keeps json-source one honest thing.
Malformed JSON is likewise a hard error (exit 2), never a partial or repaired parse.
CANONICAL RE-SERIALIZATION (declared, so it is pinnable). Each element is emitted via
canonical compact JSON.stringify — object key order is preserved from the input, but
input WHITESPACE and NUMBER TOKENS are normalized to canonical JSON form (`1e3` -> 1000,
`1.0` -> 1, `[ 1 ,2 ]` -> [1,2]). This is the JSON *value*, losslessly; it is not the
input's exact bytes. The result is deterministic: the same input yields byte-identical
output every run and every machine.
THE MODEL
[FILE] The JSON file to stream. If omitted, read stdin. Its top-level value must
be an array; each element becomes one output line.
DETERMINISM. parse(text) is a pure function — no clock, no randomness, no network — so
the same text yields byte-identical output every run.
USAGE
node json-source.js data.json
cat data.json | node json-source.js
node json-source.js --help
Exit codes: 0 success (including an empty stream from `[]`) · 2 input error (malformed
JSON, a top-level value that is not an array, an unknown option, a second positional
file, or an unreadable file). Always a clean one-line message on stderr, never a stack
trace.
Released under MIT. Its edge is printed in the README: json-source streams the elements
of a TOP-LEVEL JSON ARRAY only — it refuses a non-array top-level and malformed JSON
(exit 2), and it re-serializes each element to canonical compact JSON (normalizing
number tokens and whitespace, preserving key order). It reads JSON, not JSON5/NDJSON,
and does not stream incrementally (it parses the whole document).
*/
"use strict";
/* ---- the pure core ------------------------------------------------ */
// Parse JSON text and return its top-level array of elements. Throws a clean Error on
// malformed JSON or a non-array top-level — the CLI turns that into exit 2. Pure.
function parse(text) {
if (typeof text !== "string") throw new Error("input must be text");
var value;
try {
value = JSON.parse(text);
} catch (e) {
throw new Error("input is not valid JSON: " + e.message);
}
if (!Array.isArray(value)) {
var kind = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
throw new Error("top-level JSON must be an array (got " + kind + ") — a non-array value is not a stream");
}
return value;
}
// Render elements as JSONL text (one canonical compact JSON value per line, trailing
// newline if any).
function toJSONL(elements) {
var s = "";
for (var i = 0; i < elements.length; i++) s += JSON.stringify(elements[i]) + "\n";
return s;
}
/* ---- exports (browser + Node) ------------------------------------ */
if (typeof window !== "undefined") {
window.ForestGifts = window.ForestGifts || {};
window.ForestGifts.jsonSource = { parse: parse, toJSONL: toJSONL };
}
if (typeof module !== "undefined" && module.exports) {
module.exports = { parse: parse, toJSONL: toJSONL };
}
/* ---- CLI (runs only when invoked directly, never on require) ------ */
function parseArgs(args) {
var file;
var i = 0;
while (i < args.length) {
var a = args[i];
if (a.charAt(0) === "-" && a !== "-") {
throw new Error("unknown option " + a);
} else {
if (file !== undefined) throw new Error("only one input file may be given (got a second: " + JSON.stringify(a) + ")");
file = a;
i += 1;
}
}
return { file: file };
}
function readAll(stream) {
return new Promise(function (resolve, reject) {
var chunks = [];
stream.on("data", function (c) { chunks.push(c); });
stream.on("end", function () { resolve(Buffer.concat(chunks).toString("utf8")); });
stream.on("error", reject);
});
}
function helpText() {
return (
"json-source.js — stream the elements of a JSON array as a JSONL record stream.\n\n" +
" node json-source.js data.json\n" +
" cat data.json | node json-source.js\n" +
" node json-source.js --help\n\n" +
" [FILE] JSON file to stream (default: read stdin). Top-level must be an array.\n\n" +
"Emits one canonical compact JSON line per top-level array element.\n\n" +
"Edge: streams a TOP-LEVEL JSON ARRAY only — a non-array top-level or malformed JSON\n" +
"is refused (exit 2). Each element is re-serialized to canonical compact JSON (number\n" +
"tokens and whitespace normalized, key order preserved). Reads JSON, not JSON5/NDJSON,\n" +
"and parses the whole document (not an incremental stream).\n"
);
}
function main(argv) {
var args = argv.slice(2);
if (args.indexOf("--help") !== -1 || args.indexOf("-h") !== -1) {
process.stdout.write(helpText());
return Promise.resolve(0);
}
var parsed;
try { parsed = parseArgs(args); }
catch (e) { process.stderr.write("json-source: " + e.message + "\n"); return Promise.resolve(2); }
var getText;
if (parsed.file !== undefined) {
getText = new Promise(function (resolve, reject) {
require("fs").readFile(parsed.file, "utf8", function (err, data) {
if (err) reject(new Error("cannot read " + JSON.stringify(parsed.file) + ": " + err.code));
else resolve(data);
});
});
} else {
getText = readAll(process.stdin);
}
return getText.then(function (text) {
var elements = parse(text); // throws -> caught below
process.stdout.write(toJSONL(elements));
return 0;
}).catch(function (e) {
process.stderr.write("json-source: " + e.message + "\n");
return 2;
});
}
if (typeof require !== "undefined" && require.main === module) {
main(process.argv).then(function (code) { process.exitCode = code; });
}
test_json-source.js108 lineson GitHub →
#!/usr/bin/env node
/* test_json-source.js — battery for the json-source gift.
node test_json-source.js -> exit 0 PASS / non-zero FAIL. Zero dependencies.
Cross-checks the gift against an INDEPENDENT map-route oracle (a different route than
the gift's loop), frozen hand goldens, the canonical-re-serialization contract,
determinism, and every fail-closed edge. */
"use strict";
var js = require("./json-source.js");
var pass = 0, fail = 0;
function ok(name, cond) { if (cond) pass++; else { fail++; console.log(" FAIL " + name); } }
function eqS(name, a, b) {
if (a === b) pass++;
else { fail++; console.log(" FAIL " + name + "\n exp " + JSON.stringify(b) + "\n got " + JSON.stringify(a)); }
}
function throws(name, fn) {
var threw = false; try { fn(); } catch (e) { threw = true; }
if (threw) pass++; else { fail++; console.log(" FAIL " + name + " (expected a throw)"); }
}
/* ---- independent oracle: Array.prototype.map route (different route than the gift) ---- */
function oracleJSONL(text) {
var v = JSON.parse(text); // may throw -> caller counts as error
if (!Array.isArray(v)) throw new Error("not array");
return v.map(function (el) { return JSON.stringify(el); }).join("") + (v.length ? "" : "");
}
// build expected JSONL the oracle way (join with \n + trailing \n if non-empty)
function oracle(text) {
var v = JSON.parse(text);
if (!Array.isArray(v)) throw new Error("not array");
var out = "";
v.forEach(function (el) { out += JSON.stringify(el) + "\n"; });
return out;
}
/* ---- 1. differential grid: gift == oracle on every good vector ---- */
var good = [
"[]",
"[1,2,3]",
'[{"id":1},{"id":2}]',
'[{"a":1,"b":2},{"a":3,"b":4}]',
'["x","y","z"]',
"[true,false,null]",
'[{"nested":{"k":[1,2]}}]',
"[ 1 , 2 ,\n 3 ]", // whitespace normalized
"[1e3, 1.0, 100]", // number tokens normalized to value
'[{"b":2,"a":1}]', // key order preserved from input
'[""]', // array with one empty string
];
for (var i = 0; i < good.length; i++) {
eqS("differential[" + i + "] gift==oracle", js.toJSONL(js.parse(good[i])), oracle(good[i]));
}
/* ---- 2. hand goldens (frozen) ---- */
eqS("golden: empty array -> empty stream", js.toJSONL(js.parse("[]")), "");
eqS("golden: scalars", js.toJSONL(js.parse("[1,2,3]")), "1\n2\n3\n");
eqS("golden: objects", js.toJSONL(js.parse('[{"id":1},{"id":2}]')), '{"id":1}\n{"id":2}\n');
eqS("golden: whitespace normalized", js.toJSONL(js.parse("[ 1 , 2 ]")), "1\n2\n");
eqS("golden: number token 1e3 -> 1000", js.toJSONL(js.parse("[1e3]")), "1000\n");
eqS("golden: number token 1.0 -> 1", js.toJSONL(js.parse("[1.0]")), "1\n");
eqS("golden: key order preserved", js.toJSONL(js.parse('[{"b":2,"a":1}]')), '{"b":2,"a":1}\n');
eqS("golden: nested value verbatim-as-value", js.toJSONL(js.parse('[{"k":{"z":[1,2]}}]')), '{"k":{"z":[1,2]}}\n');
/* ---- 3. one line per element ---- */
var recs = js.parse('[{"a":1},{"a":2},{"a":3}]');
ok("count: 3 elements", recs.length === 3);
ok("lines: JSONL has 3 lines", js.toJSONL(recs).split("\n").filter(Boolean).length === 3);
/* ---- 4. fail-closed (exit-2 class) ---- */
throws("bad: malformed JSON", function () { js.parse("[1,2,"); });
throws("bad: top-level object", function () { js.parse('{"a":1}'); });
throws("bad: top-level scalar number", function () { js.parse("42"); });
throws("bad: top-level string", function () { js.parse('"hello"'); });
throws("bad: top-level bool", function () { js.parse("true"); });
throws("bad: top-level null", function () { js.parse("null"); });
throws("bad: empty input (not JSON)", function () { js.parse(""); });
throws("bad: non-string input", function () { js.parse(123); });
/* ---- 5. determinism: parse twice -> byte-identical JSONL ---- */
var arr = [];
for (var k = 0; k < 50; k++) arr.push({ n: k, s: "v" + k });
var big = JSON.stringify(arr);
ok("determinism: two parses byte-identical", js.toJSONL(js.parse(big)) === js.toJSONL(js.parse(big)));
/* ---- 6. toJSONL shape ---- */
ok("toJSONL: one value per line + trailing nl", js.toJSONL([{ a: 1 }]) === '{"a":1}\n');
ok("toJSONL: empty -> empty string", js.toJSONL([]) === "");
/* ---- 7. mutation tripwire: an object-flattening mutant must diverge ---- */
(function () {
// mutant: emits only the FIRST value of each object (a lossy "flatten"); must diverge
function mutant(text) {
var v = JSON.parse(text); var out = "";
v.forEach(function (el) {
if (el && typeof el === "object" && !Array.isArray(el)) {
var ks = Object.keys(el); out += JSON.stringify(ks.length ? el[ks[0]] : el) + "\n";
} else out += JSON.stringify(el) + "\n";
});
return out;
}
var v = '[{"a":1,"b":2}]';
ok("mutation: first-value-flatten is CAUGHT (diverges from gift)",
mutant(v) !== js.toJSONL(js.parse(v)));
})();
console.log((fail === 0 ? "PASS" : "FAIL") + " " + pass + "/" + (pass + fail));
process.exit(fail === 0 ? 0 : 1);