A client can fail.
An object field was removed, or a value changed type, including to or from null.
price: 5 → "5.00"
v0.1.0 · lossless, redaction-safe
Compare two JSON documents and see what moved, how much it matters, and get a diff you can paste into a pull request without leaking a secret.
npm install --save-dev m2jsonlenschanged: string [REDACTED] → string [REDACTED]
added: absent → string "AED"
changed: string [REDACTED] → string [REDACTED]
type changed: null [REDACTED] → string [REDACTED]
Real output from compare. 9 differences, secrets redacted.
Impact levels
Every change carries a kind and an impact, decided from the shape of the data, so you can fail a build on breaking changes and still see everything else.
An object field was removed, or a value changed type, including to or from null.
price: 5 → "5.00"
A new object field appeared. Anything written against the baseline keeps working.
added currency
A scalar changed, or an array element was added, removed or edited. Often expected, still worth seeing.
Structural only: it can't know what is semantic for you.
What you get
A side that doesn't exist is absent, not null. Every difference is classed breaking, additive or data, so a reviewer knows what to look at first.
Integers beyond 2^53 stay exact, and 1, 1.0 and 1e0 are equal. JSON.parse would round the first and can't tell the others apart.
Listed paths never appear in text, JSON, added subtrees or errors. No hashes either, since hashes of weak secrets can be brute-forced.
Ordered by default. Switch a path to unordered, or key it by a field like sku, and reordering stops being noise.
Optional Ajv-backed validation that explains what's wrong without echoing the document's values.
Exit codes 0, 1 and 2, stable sorted JSON, stdin support and a plain-JSON config that rejects typos. Zero runtime dependencies.
Workflow
Point it at a saved golden response and a fresh one. It reads both into memory, so it is built for API responses and configs, not multi-gigabyte streams.
Parse both documents losslessly and walk them together.
m2jsonlens diff a.json b.jsonHide only what you listed, so the output is safe to paste.
--redact /auth/tokenLet the exit code decide whether the pull request can merge.
--fail-on breakingPaths are not redacted: object keys appear in them. Read the redaction guarantees and limits →
How it works
Use - to read one side from stdin. Exit codes tell your pipeline what happened.
m2jsonlens diff baseline.json candidate.json --config m2jsonlens.config.json
curl -s $URL | m2jsonlens diff golden.json - # "-" reads one side from stdin
m2jsonlens validate response.json --schema orders.schema.jsonPlain JSON, never code. Unknown keys are errors, so typos are caught.
{
"ignore": ["/requestId", "**/updatedAt"],
"redact": ["/order/auth/token", "/order/customer/email"],
"arrays": {
"default": "ordered",
"rules": [
{ "path": "/order/items", "mode": "keyed", "key": "sku" },
{ "path": "/order/tags", "mode": "unordered" }
]
},
"failOn": "breaking"
}The result is plain data you can filter and test. ESM and CommonJS are both shipped.
import { compare, formatText, parseJson } from "m2jsonlens";
declare const baselineText: string;
declare const candidateText: string;
const result = compare(parseJson(baselineText), parseJson(candidateText), {
ignore: ["/requestId"],
redact: ["/auth/token"],
arrays: { rules: [{ path: "/items", mode: "keyed", key: "sku" }] },
});
if (result.summary.breaking > 0) console.error(formatText(result));- run: npm ci
- run: npx m2jsonlens diff golden.json <(curl -s $URL) --config m2jsonlens.config.json --fail-on breakingPlayground
This runs the m2jsonlens source from this repository in your browser. Nothing is mocked and nothing is uploaded.
string [REDACTED] → string [REDACTED]
(absent) → string "AED"
string [REDACTED] → string [REDACTED]
null [REDACTED] → string [REDACTED]
null null → (absent)
number "9007199254740993" → number 9007199254740992
number 2 → number 3
number 5 → string "5.00"
(absent) → object {"sku":"C-3","qty":1,"price":1}
A field disappeared, a number became a string and a phone went from null to a value. Secrets are listed in redact, so their values never appear, even though the token changed. Items are matched by sku, so reordering is not a difference. How the comparison works →
import { compare, formatText, parseJson, type CompareOptions, type CompareResult } from "m2jsonlens";
export type Input = {
baseline: string;
candidate: string;
options: CompareOptions;
};
export type Outcome = { result: CompareResult; text: string };
/** The same functions the CLI uses. Both documents are parsed losslessly, so large numbers stay exact. */
export function diff({ baseline, candidate, options }: Input): Outcome {
const result = compare(parseJson(baseline), parseJson(candidate), options);
return { result, text: formatText(result) };
}Options
The defaults assume nothing: array order matters, and no field is treated as a secret until you say so.
ignorePaths excluded from comparison. * matches one segment, ** matches any number.redactPaths whose values never appear anywhere in the output. Nothing is guessed.arrays: orderedThe default. Array order is significant unless a rule says otherwise.arrays: unorderedElements match as a multiset, duplicates counted.arrays: keyedElements match by a field such as sku, and diff at field level.--fail-onany, breaking or none: decides which differences make the exit code non-zero.Documentation
More from M2Toolkit