package.json diff
Developer tools
Loading
Loading tool
The tool is loaded only when you open it.
All processing for this tool happens in your browser. Your input is not sent to a server.
About this tool
Compare two package.json documents as structured declarations. The report groups dependencies by package name across dependencies, devDependencies, optionalDependencies and peerDependencies, then shows scripts, engines and other top-level metadata separately. Each change has before and after values and one of four labels: added, removed, changed or moved. This helps review a proposed manifest edit without running a package manager or executing script text. All parsing and comparison happen in the current browser page.
Common uses
- Review a pull request that moves a build tool between development and runtime dependency sections while changing its declared range.
- Check edits to build or test scripts, Node.js engine declarations, packageManager, overrides and package entry-point metadata before committing them.
- Prepare a deterministic JSON summary of all declaration changes while filtering the screen to the category you are reviewing.
How to use it
- 1.Paste the old and new package.json text, or load the example. Each side must be a strict JSON object. Comments, trailing commas and duplicate decoded keys are rejected.
- 2.Compare the manifests, then inspect counts and before/after values. Filter by change kind or field category; large result sets are displayed 100 changes per page. A moved package may also have a changed specifier.
- 3.Review the secret warning before copying or downloading the complete JSON report. Display filters never remove exported rows. Editing either input or changing the interface language clears the previous result.
Reproducible manifest comparisons
Formatting and ordinary key order
{
"before": "{\n \"config\": {\n \"b\": 2,\n \"a\": 1\n },\n \"dependencies\": {\n \"beta\": \"2.0.0\",\n \"alpha\": \"1.0.0\"\n }\n}",
"after": "{\n \"dependencies\": {\n \"alpha\": \"1.0.0\",\n \"beta\": \"2.0.0\"\n },\n \"config\": {\n \"a\": 1,\n \"b\": 2\n }\n}"
}{
"error": null,
"counts": {
"added": 0,
"removed": 0,
"changed": 0,
"moved": 0
}
}Both manifests contain the same declarations. Moving ordinary keys does not produce a change; the output shown is the error-and-count summary.
Add, remove and change dependencies
{
"before": "{\n \"dependencies\": {\n \"demo\": \"^1.0.0\"\n },\n \"devDependencies\": {\n \"old\": \"1.0.0\"\n }\n}",
"after": "{\n \"dependencies\": {\n \"demo\": \"^2.0.0\"\n },\n \"optionalDependencies\": {\n \"extra\": \"~1.2.0\"\n }\n}"
}{
"error": null,
"counts": {
"added": 1,
"removed": 1,
"changed": 1,
"moved": 0
}
}extra is added, old is removed and demo changes its literal SemVer range. These are declaration changes; no installed versions are inferred.
Move and change a specifier together
{
"before": "{\n \"devDependencies\": {\n \"demo\": \"^1.0.0\"\n }\n}",
"after": "{\n \"dependencies\": {\n \"demo\": \"^2.0.0\"\n }\n}"
}{
"error": null,
"counts": {
"added": 0,
"removed": 0,
"changed": 0,
"moved": 1
}
}demo remains present, but moves from devDependencies to dependencies while its range changes. It counts once as moved; the full report retains both section maps.
Scripts, engine and package-manager edits
{
"before": "{\n \"scripts\": {\n \"build\": \"vite build\"\n },\n \"engines\": {\n \"node\": \">=20\"\n },\n \"packageManager\": \"npm@10.8.0\"\n}",
"after": "{\n \"scripts\": {\n \"build\": \"tsc && vite build\",\n \"test\": \"node --test\"\n },\n \"engines\": {\n \"node\": \">=22\"\n },\n \"packageManager\": \"npm@11.0.0\"\n}"
}{
"error": null,
"counts": {
"added": 1,
"removed": 0,
"changed": 3,
"moved": 0
}
}One test script is added. The build command, Node.js engine declaration and packageManager value change in their respective categories. No command runs.
Conditional key priority and array order
{
"before": "{\n \"exports\": {\n \".\": {\n \"import\": \"./a.js\",\n \"default\": \"./b.js\"\n }\n },\n \"files\": [\n \"dist\",\n \"README.md\"\n ]\n}",
"after": "{\n \"exports\": {\n \".\": {\n \"default\": \"./b.js\",\n \"import\": \"./a.js\"\n }\n },\n \"files\": [\n \"README.md\",\n \"dist\"\n ]\n}"
}{
"error": null,
"counts": {
"added": 0,
"removed": 0,
"changed": 2,
"moved": 0
}
}The exports condition order changes and files is reordered, yielding two metadata changes. This conservative structural comparison does not decide whether runtime behavior actually changes.
Non-registry and alias declarations
{
"before": "{\n \"dependencies\": {\n \"demo\": \"^1.0.0\"\n }\n}",
"after": "{\n \"dependencies\": {\n \"demo\": \"file:../demo\",\n \"alias\": \"npm:demo@^2.0.0\",\n \"local\": \"workspace:*\",\n \"source\": \"github:example/demo#main\"\n }\n}"
}{
"error": null,
"counts": {
"added": 3,
"removed": 0,
"changed": 1,
"moved": 0
}
}The alias, workspace and Git entries are added; demo changes to a local file specifier. The syntax labels never cause a download, filesystem read or package resolution.
Reject duplicate decoded keys
{
"before": "{\"name\":\"demo\",\"na\\u006de\":\"other\"}",
"after": "{}"
}{
"error": {
"side": "before",
"code": "duplicateKey"
},
"counts": {
"added": 0,
"removed": 0,
"changed": 0,
"moved": 0
}
}name and na\u006de decode to the same key. The entire comparison stops on the before input; no overwritten value or partial result is used.
Common manifest-review mistakes
- Pasting a lockfile, JavaScript object, JSONC comments or trailing commas instead of strict package.json JSON.
- Treating a specifier label or changed range as an installed version, upgrade recommendation or security finding.
- Reading a move count as the number of individual section edits: it counts each affected package name once.
- Expecting the filtered display to redact or exclude exported changes. The complete report retains raw values, including secrets.
- Ignoring condition order in exports/imports or assuming scripts shown in a diff were executed.
Limits and notes
- This is a bounded declaration diff, not an npm schema validator, dependency resolver or installed-version comparison. It does not read lockfiles, fetch packages, inspect the registry, execute scripts, or establish compatibility, vulnerability status or an upgrade/downgrade. Semantically equivalent range strings can still differ.
- Each input is limited to 262,144 UTF-8 bytes, depth 32 and 20,000 JSON values. The parser rejects duplicate keys at any depth, invalid JSON, non-object roots, non-finite numbers, unsafe integers and numbers that cannot round-trip without decimal change. Dependency sections, scripts and engines must contain string values; name, version and packageManager must be strings, and overrides must be an object when present. Any error stops comparison without a partial diff.
- Dependencies are aggregated across the four supported sections. Added or removed means the package name appears on only one side. Different section membership is one moved row, even with a specifier change or continued membership elsewhere. Equal membership with different literal values is changed. Empty dependency/script/engine sections and absent sections compare the same; overrides and peerDependenciesMeta remain metadata rather than resolved dependency changes.
- Ordinary object-key order and JSON formatting are ignored; string content and array order are preserved. All nested object-key order within exports and imports is preserved because conditional priority may matter. Other metadata is compared once per top-level field, so a nested edit shows that field’s complete structured before/after values. Specifier classification is informational, including unknown syntax, and does not validate or resolve package targets. Integer-index object keys anywhere under exports/imports are rejected to avoid JavaScript reordering them and hiding a condition-order change.
- Inputs are not uploaded or persisted by this tool. JSON exports contain the raw changed field values, including private URLs, script commands and any secrets, without redaction. All changes are exported regardless of filters or the current page. Once copied, saved or shared, that report is outside the page’s control.
Frequently asked questions
Does a changed dependency mean it was upgraded?
No. The tool compares literal declarations. ^1.0.0 becoming ~1.5.0, latest becoming next, or a registry range becoming a file path does not identify the installed version. A lockfile and package-manager resolution are outside this comparison.
How is moving a dependency different from removing and adding it?
The same package name is grouped across all four dependency sections. If it exists on both sides but its section membership changes, the result is one move. The two section maps also expose any specifier change; a package present in several sections is not silently collapsed to a single value.
Why can reordered JSON sometimes produce a change?
Formatting and normal object-key order are ignored. Arrays keep their order, and exports/imports objects keep nested key order because conditional matching priority can depend on it. Reordering conditions therefore remains visible even if all values are unchanged.
Are workspace, alias, Git and local-path declarations supported?
They are retained as literal strings and classified as workspace, npm alias, Git or local file/path. Exact versions, SemVer ranges, URLs, distribution tags and unknown syntax also get labels. No target is contacted, executed or resolved, and a label is not a validity or safety guarantee.