This website is meant to be read and understood quickly by humans, but is only fully parsable, on a technical level, with the aid of an AI system. Read why →
Loop MMT
Keep Only the Records That Match Your Shapefilter← all gifts

schema-filter

schema-filter reads a JSONL object stream and emits, verbatim and in input order, exactly the records that match a declared JSON shape — a subset filter you drop into a pipe as a shape-checking gate. Declare --field NAME TYPE (required) or --optional NAME TYPE over a closed set of seven JSON types (string, number, integer, boolean, object, array, null); constraints AND together, no constraints is the identity filter. The pure core has zero dependencies and runs unchanged in Node or a browser, and the same stream and shape yield byte-identical survivors every run.

The honest edge
A SUBSET filter over a DECLARED shape: it filters on the fields and types YOU name — it does not infer a schema from the data, reshape, sort, or de-duplicate. It tests a value's TYPE, never its truth: a record matching every declared type passes even if its values are wrong. The string "5" is not the number 5 (no coercion); 3.5 is a number but not an integer; a required field's absence DROPS the record while an optional field's absence passes; an unknown type is a startup error, so a typo'd shape is loud, not a silently-empty result.
Run it
cat data.jsonl | node schema-filter.js --field name string --field age integer test_schema-filter.js (75/75, independent from-spec oracle) + Plumb conformance GREEN (15/15, signed 2026-09-12, clock-independent, mutation-bite non-vacuous) Node / browser, no dependencies
The code — every file that ships
schema-filter.js245 lineson GitHub →
#!/usr/bin/env node
/* schema-filter.js — pass only the JSONL records that match a declared JSON shape.
   Dependency-free, deterministic, one pass, order-stable. Runs in Node or a browser. MIT.

   WHAT IT IS. Give it a stream of records — one JSON object per line (JSONL) — and a
   SHAPE you declare as a set of typed fields, and it emits, verbatim and in input
   order, exactly the records that match EVERY declared field. Everything else is
   dropped. It is a SUBSET filter: the output is a subset of the input, byte-for-byte
   per surviving record, so `schema-filter` in a pipe never invents or reshapes a
   record — it only decides which ones pass.

   A FIELD CONSTRAINT names one field and the JSON TYPE it must hold:

       --field    NAME TYPE     require NAME present AND of TYPE (fail-closed if absent)
       --optional NAME TYPE     if NAME is present it must be TYPE; if absent, it passes

   TYPE is one of a documented, closed set of seven JSON types:

       string   a JSON string
       number   a finite JSON number (integer or fractional)
       integer  a finite JSON number with no fractional part
       boolean  true or false
       object   a JSON object (not an array, not null)
       array    a JSON array
       null     the JSON literal null

   Declare more than one field and they AND together: a record survives only if it
   matches all of them. Declare none and every well-formed record passes (the identity
   filter) — a deliberate, documented default, not an error.

   THE MISSING-FIELD POLICY (the whole honesty story). A `--field` constraint is a
   claim the record must satisfy, and a record that LACKS that field cannot be shown to
   satisfy it, so it is DROPPED. This is fail-closed — absence is not a match. Use
   `--optional` for the "if this field is present it must be a date, otherwise leave the
   record alone" shape: an optional field that is absent passes; an optional field that
   is present but the WRONG type still fails. The choice is per field, declared once, so
   a stream is filtered under one explicit shape.

   TYPE, NOT VALUE. schema-filter tests a value's TYPE, never its meaning. A record with
   the right types passes even if the values are nonsense: `{"age": -999}` matches
   `--field age integer`. It does not coerce (the string "5" does NOT match `number`),
   does not infer a schema from the data, does not reshape, sort, or de-duplicate
   surviving records. `number` excludes non-finite values by construction (valid JSON
   cannot carry NaN or Infinity, so this only ever matters for a hostile embedder).
   `integer` is a type test (no fractional part), never a rounding.

   DETERMINISM. Records are tested and emitted in input order; a surviving record is
   written back byte-for-byte as it arrived (the original line, trailing \r trimmed), so
   the same stream and the same shape yield byte-identical output on every machine and
   every run.

   STRICTNESS. A filter is only trustworthy if it refuses to guess:
     - Every non-blank line must be a JSON OBJECT. A line that is not valid JSON, or is
       valid JSON but not an object (a number, string, array, null), is a HARD ERROR
       (exit 2) naming the line — never a silent skip.
     - An unknown TYPE token in a `--field`/`--optional` declaration is a hard error
       (exit 2) at startup, before any record is read — a shape you cannot express is a
       bug in the caller's declaration, surfaced loudly, not an empty result.

   USAGE
     cat people.jsonl | node schema-filter.js --field name string --field age integer
     node schema-filter.js --field id string --optional email string in.jsonl
     node schema-filter.js --field tags array < items.jsonl
     node schema-filter.js --help

   Each non-blank line is one JSON object. Blank lines are skipped. A trailing \r
   (CRLF files) is trimmed. Surviving records are emitted one per line, verbatim.

   Exit codes: 0 success (with or without survivors) · 2 input error (missing file, a
   directory, a malformed constraint, an unknown type, or a line that is not a JSON
   object). Always a clean one-line message on stderr, never a stack trace.

   Released under MIT. Its edge is printed in the README: this is a SUBSET filter over a
   DECLARED shape. It filters on the fields and types YOU name — it does not infer a
   schema, does not reshape or reformat surviving records, does not sort, and does not
   de-duplicate. A record matching every declared type passes even if its values are
   wrong: the constraint tests a value's TYPE, never its truth.
*/
"use strict";

/* ---- the pure core ------------------------------------------------ */

// The closed set of declarable JSON types. A shape you cannot express is refused.
var TYPES = {
  string: 1, number: 1, integer: 1, boolean: 1, object: 1, array: 1, "null": 1
};

// Does one JSON value match one declared type? Total over the seven types;
// an out-of-set type never matches (the CLI refuses those before this runs).
function matchType(val, type) {
  switch (type) {
    case "string":  return typeof val === "string";
    case "number":  return typeof val === "number" && isFinite(val);
    case "integer": return typeof val === "number" && isFinite(val) && Math.floor(val) === val;
    case "boolean": return typeof val === "boolean";
    case "object":  return val !== null && typeof val === "object" && !Array.isArray(val);
    case "array":   return Array.isArray(val);
    case "null":    return val === null;
  }
  return false;
}

// A parsed constraint: { field, type, optional }. Does one record satisfy it?
function satisfies(rec, c) {
  var has = Object.prototype.hasOwnProperty.call(rec, c.field);
  if (!has) return !!c.optional;            // missing: required -> fail, optional -> pass
  return matchType(rec[c.field], c.type);   // present: must match the declared type
}

// The public filter: JSONL text + parsed constraints -> the surviving records,
// in input order, each as { line, record }.
//   constraints: [ {field, type, optional}, ... ]
// Throws a clean, line-named Error on a line that is not a JSON object.
function filter(text, constraints) {
  var lines = String(text).split("\n");
  var out = [];
  var i, line, rec;

  for (i = 0; i < lines.length; i++) {
    line = lines[i];
    if (line.charCodeAt(line.length - 1) === 0x0d) line = line.slice(0, -1); // trim \r
    if (line.length === 0) continue; // blank line is not a record

    try { rec = JSON.parse(line); }
    catch (e) {
      throw new Error("line " + (i + 1) + " is not valid JSON: " +
        JSON.stringify(line.slice(0, 40)));
    }
    if (rec === null || typeof rec !== "object" || Array.isArray(rec)) {
      throw new Error("line " + (i + 1) + " is not a JSON object (got " +
        (rec === null ? "null" : Array.isArray(rec) ? "array" : typeof rec) + "): " +
        JSON.stringify(line.slice(0, 40)));
    }

    var pass = true;
    for (var k = 0; k < constraints.length; k++) {
      if (!satisfies(rec, constraints[k])) { pass = false; break; }
    }
    if (pass) out.push({ line: line, record: rec });
  }
  return out;
}

/* ---- exports (browser + Node) ------------------------------------ */
if (typeof window !== "undefined") {
  window.ForestGifts = window.ForestGifts || {};
  window.ForestGifts.schemaFilter = filter;
  window.ForestGifts.schemaFilterMatchType = matchType;
}
if (typeof module !== "undefined" && module.exports) {
  module.exports = { filter: filter, satisfies: satisfies, matchType: matchType };
}

/* ---- CLI (runs only when invoked directly, never on require) ------ */

// Parse argv into { constraints, files }. Throws a clean Error on a bad declaration.
function parseArgs(args) {
  var constraints = [];
  var files = [];
  var i = 0;
  while (i < args.length) {
    var a = args[i];
    if (a === "--field" || a === "--optional") {
      var optional = a === "--optional";
      var field = args[i + 1], type = args[i + 2];
      if (field === undefined || type === undefined || field.charAt(0) === "-") {
        throw new Error(a + " requires FIELD TYPE (e.g. " + a + " age integer)");
      }
      if (!Object.prototype.hasOwnProperty.call(TYPES, type)) {
        throw new Error(a + " " + field + ": unknown type " + JSON.stringify(type) +
          " (declare one of: string number integer boolean object array null)");
      }
      constraints.push({ field: field, type: type, optional: optional });
      i += 3;
    } else if (a.charAt(0) === "-") {
      throw new Error("unknown option " + a);
    } else {
      files.push(a); i += 1;
    }
  }
  return { constraints: constraints, files: files };
}

function run(text, constraints) {
  var survivors = filter(text, constraints);
  var s = "";
  for (var i = 0; i < survivors.length; i++) s += survivors[i].line + "\n";
  return s;
}

function main(argv) {
  var args = argv.slice(2);
  if (args.indexOf("--help") !== -1 || args.indexOf("-h") !== -1) {
    process.stdout.write(
      "schema-filter.js — pass only the JSONL records that match a declared JSON shape.\n\n" +
      "  cat people.jsonl | node schema-filter.js --field name string --field age integer\n" +
      "  node schema-filter.js --field id string --optional email string in.jsonl\n" +
      "  node schema-filter.js --field tags array < items.jsonl\n" +
      "  node schema-filter.js --help\n\n" +
      "  --field    NAME TYPE   require NAME present AND of TYPE (fail-closed if absent)\n" +
      "  --optional NAME TYPE   if NAME is present it must be TYPE; if absent, it passes\n\n" +
      "TYPE is one of: string number integer boolean object array null\n" +
      "Multiple fields AND together. No fields = every well-formed record passes.\n" +
      "Surviving records are emitted VERBATIM, in input order.\n\n" +
      "Edge: this is a SUBSET filter over a DECLARED shape. It tests a value's TYPE,\n" +
      "never its meaning; it does not coerce, infer a schema, reshape, sort, or\n" +
      "de-duplicate. Absence fails a --field and passes an --optional. A non-object\n" +
      "line or an unknown type is a hard error (exit 2).\n"
    );
    return 0;
  }

  var parsed;
  try { parsed = parseArgs(args); }
  catch (e) { process.stderr.write("schema-filter: " + e.message + "\n"); return 2; }

  function emit(text) {
    try { process.stdout.write(run(text, parsed.constraints)); return 0; }
    catch (e) { process.stderr.write("schema-filter: " + e.message + "\n"); return 2; }
  }

  if (parsed.files.length > 0) {
    var fs = require("fs");
    var text;
    try { text = fs.readFileSync(parsed.files[0], "utf8"); }
    catch (e) {
      process.stderr.write("schema-filter: cannot read " + parsed.files[0] +
        " (" + (e.code === "EISDIR" ? "is a directory" : (e.code || "read error")) + ")\n");
      return 2;
    }
    return emit(text);
  }

  // stdin
  var chunks = [];
  process.stdin.on("data", function (d) { chunks.push(d); });
  process.stdin.on("end", function () {
    process.exitCode = emit(Buffer.concat(chunks).toString("utf8"));
  });
  return 0;
}

if (typeof require !== "undefined" && require.main === module) {
  process.exitCode = main(process.argv);
}
test_schema-filter.js170 lineson GitHub →
#!/usr/bin/env node
/* test_schema-filter.js — out-of-band battery for the schema-filter gift.

   Exercises the exported pure core (filter / satisfies / matchType) against an
   INDEPENDENTLY authored oracle — a second, from-spec type checker written with a
   different route (a typeof/predicate map, not the gift's switch) — plus hand goldens
   and every documented edge. Node only; no dependencies. Exit 0 all-pass / 1 fail.
*/
"use strict";
var sf = require("./schema-filter.js");

var pass = 0, fail = 0;
function check(name, cond) {
  if (cond) { pass++; }
  else { fail++; console.log("  FAIL  " + name); }
}
function J(v) { return JSON.stringify(v); }

/* ---- independent oracle: a second, from-spec type checker ---------------- */
// Different route than the gift's switch: an explicit predicate table.
var ORACLE = {
  string:  function (v) { return typeof v === "string"; },
  number:  function (v) { return typeof v === "number" && v === v && v !== Infinity && v !== -Infinity; },
  integer: function (v) { return typeof v === "number" && v === v && v !== Infinity && v !== -Infinity && parseInt(v, 10) === v && v % 1 === 0; },
  boolean: function (v) { return v === true || v === false; },
  object:  function (v) { return typeof v === "object" && v !== null && !(v instanceof Array); },
  array:   function (v) { return v instanceof Array; },
  "null":  function (v) { return v === null; }
};
function oracleSatisfies(rec, c) {
  var has = Object.prototype.hasOwnProperty.call(rec, c.field);
  if (!has) return !!c.optional;
  return ORACLE[c.type](rec[c.field]);
}
function oracleSurvivors(recs, constraints) {
  return recs.filter(function (r) {
    return constraints.every(function (c) { return oracleSatisfies(r, c); });
  }).map(function (r) { return JSON.stringify(r); });
}
// Drive the gift's pure filter over a JSONL rendering of the records.
function giftSurvivors(recs, constraints) {
  var text = recs.map(function (r) { return JSON.stringify(r); }).join("\n") + "\n";
  return sf.filter(text, constraints).map(function (s) { return s.line; });
}

/* ---- matchType: every type, positive + negative ------------------------- */
var typeVectors = [
  ["string",  "hi", true], ["string", 5, false], ["string", null, false],
  ["number",  5, true], ["number", 5.5, true], ["number", "5", false], ["number", true, false],
  ["integer", 5, true], ["integer", 5.0, true], ["integer", 5.5, false], ["integer", "5", false],
  ["boolean", true, true], ["boolean", false, true], ["boolean", 0, false], ["boolean", "true", false],
  ["object",  {}, true], ["object", { a: 1 }, true], ["object", [], false], ["object", null, false],
  ["array",   [], true], ["array", [1, 2], true], ["array", {}, false], ["array", "x", false],
  ["null",    null, true], ["null", 0, false], ["null", "", false], ["null", {}, false]
];
typeVectors.forEach(function (v) {
  check("matchType(" + J(v[1]) + ", " + v[0] + ") === " + v[2], sf.matchType(v[1], v[0]) === v[2]);
  // cross-check against the independent oracle
  check("matchType == oracle [" + v[0] + ", " + J(v[1]) + "]", sf.matchType(v[1], v[0]) === ORACLE[v[0]](v[1]));
});

/* ---- the integer/number honesty distinction (the gift's sharp edge) ------ */
check("3.5 is number, NOT integer", sf.matchType(3.5, "number") === true && sf.matchType(3.5, "integer") === false);
check("3.0 IS integer (no fractional part)", sf.matchType(3.0, "integer") === true);
check("string \"3\" is NEITHER number nor integer (no coercion)",
  sf.matchType("3", "number") === false && sf.matchType("3", "integer") === false);

/* ---- scenario battery: gift filter == independent oracle ----------------- */
var scenarios = [
  {
    name: "single --field integer",
    recs: [{ n: 1 }, { n: 2.5 }, { n: "3" }, { n: 4 }],
    constraints: [{ field: "n", type: "integer", optional: false }]
  },
  {
    name: "two --field AND (name string + age integer)",
    recs: [
      { name: "a", age: 30 }, { name: "b", age: 30.5 }, { name: 5, age: 30 },
      { name: "c" }, { age: 40 }, { name: "d", age: 40 }
    ],
    constraints: [
      { field: "name", type: "string", optional: false },
      { field: "age", type: "integer", optional: false }
    ]
  },
  {
    name: "--field required missing DROPS",
    recs: [{ id: "has", x: "y" }, { id: "missing" }],
    constraints: [{ field: "x", type: "string", optional: false }]
  },
  {
    name: "--optional absent PASSES, present-wrong FAILS",
    recs: [{ id: "absent" }, { id: "ok", email: "e" }, { id: "wrong", email: 5 }],
    constraints: [{ field: "email", type: "string", optional: true }]
  },
  {
    name: "array + object + boolean + null types",
    recs: [
      { tags: [], meta: {}, active: true, deleted: null },
      { tags: {}, meta: {}, active: true, deleted: null },
      { tags: [], meta: [], active: true, deleted: null },
      { tags: [], meta: {}, active: 1, deleted: null },
      { tags: [], meta: {}, active: true, deleted: 0 }
    ],
    constraints: [
      { field: "tags", type: "array", optional: false },
      { field: "meta", type: "object", optional: false },
      { field: "active", type: "boolean", optional: false },
      { field: "deleted", type: "null", optional: false }
    ]
  },
  {
    name: "no constraints = identity",
    recs: [{ a: 1 }, { b: 2 }, { c: 3 }],
    constraints: []
  }
];
scenarios.forEach(function (s) {
  check("gift == oracle [" + s.name + "]", J(giftSurvivors(s.recs, s.constraints)) === J(oracleSurvivors(s.recs, s.constraints)));
});

/* ---- hand goldens (independent of the oracle) ---------------------------- */
check("golden: integer keeps [1,4], drops 2.5 and \"3\"",
  J(giftSurvivors([{ n: 1 }, { n: 2.5 }, { n: "3" }, { n: 4 }], [{ field: "n", type: "integer", optional: false }]))
  === J([JSON.stringify({ n: 1 }), JSON.stringify({ n: 4 })]));
check("golden: optional absent kept, wrong-type dropped",
  J(giftSurvivors([{ id: "absent" }, { id: "wrong", email: 5 }], [{ field: "email", type: "string", optional: true }]))
  === J([JSON.stringify({ id: "absent" })]));

/* ---- verbatim passthrough + order-stability ------------------------------ */
(function () {
  var text = '{"z":1,"a":2}\n{"b":3}\n';  // key order preserved verbatim, not re-serialized
  var got = sf.filter(text, [{ field: "z", type: "integer", optional: false }]).map(function (s) { return s.line; });
  check("verbatim: original line returned (key order preserved)", J(got) === J(['{"z":1,"a":2}']));
})();
(function () {
  var recs = [{ n: 5 }, { n: 1 }, { n: 9 }, { n: 2 }];
  var got = giftSurvivors(recs, [{ field: "n", type: "integer", optional: false }]);
  check("order-stable (5,1,9,2 preserved)",
    J(got) === J([JSON.stringify({ n: 5 }), JSON.stringify({ n: 1 }), JSON.stringify({ n: 9 }), JSON.stringify({ n: 2 })]));
})();

/* ---- CRLF + blank lines -------------------------------------------------- */
(function () {
  var text = '{"n":1}\r\n\r\n{"n":2}\r\n';
  var got = sf.filter(text, [{ field: "n", type: "integer", optional: false }]).map(function (s) { return s.line; });
  check("CRLF trimmed, blank lines skipped", J(got) === J(['{"n":1}', '{"n":2}']));
})();

/* ---- strictness: non-object lines throw --------------------------------- */
function throws(text, constraints) {
  try { sf.filter(text, constraints); return false; } catch (e) { return true; }
}
check("bare number line throws", throws("42\n", []));
check("array line throws", throws("[1,2]\n", []));
check("string line throws", throws('"hi"\n', []));
check("null line throws", throws("null\n", []));
check("malformed JSON throws", throws("{bad\n", []));
check("well-formed object does NOT throw", !throws('{"a":1}\n', []));

/* ---- determinism: two runs identical ------------------------------------ */
(function () {
  var recs = scenarios[1].recs, cs = scenarios[1].constraints;
  check("deterministic across two filter() calls", J(giftSurvivors(recs, cs)) === J(giftSurvivors(recs, cs)));
})();

console.log("");
var verdict = fail === 0 ? "PASS" : "FAIL";
console.log(verdict + ": " + pass + " checks passed, " + fail + " failed  [test_schema-filter]");
process.exit(fail === 0 ? 0 : 1);
Take the whole folder → MIT Node / browser, no dependencies