JSON reporter
-r json writes what a run knows: each clone with its two locations, token count, kind and source fragment, and the statistics per format and in total. Parse it with jq or any JSON library when you build your own check or feed another tool; when GitHub or GitLab should show the clones, the SARIF and CodeClimate reporters save you the translation.
Run it
jscpd . -r json --output report
JSON report saved to report/jscpd-report.json
{
"reporters": ["console", "json"],
"output": "report"
}
The output
The file has two top-level keys, duplicates and statistics. One clone of the report below, with its fragment cut short, comes from the repository's fixtures/mcp-demo folder scanned with --ignore-identifiers --ignore-literals --max-gap-lines 1:
{
"duplicates": [
{
"format": "javascript",
"kind": "similar",
"method": "gap",
"similarity": 0.947,
"isNew": false,
"lines": 12,
"tokens": 197,
"firstFile": {
"name": "src/invoice.js",
"start": 1,
"end": 12,
"startLoc": { "line": 1, "column": 0, "position": 0 },
"endLoc": { "line": 12, "column": 1, "position": 790 }
},
"secondFile": {
"name": "src/print/invoice.js",
"start": 1,
"end": 13,
"startLoc": { "line": 1, "column": 0, "position": 0 },
"endLoc": { "line": 13, "column": 1, "position": 844 }
},
"fragment": "export function renderInvoice(invoice, customer) {\n const lines = ..."
}
],
"statistics": {
"detectionDate": "2026-10-06T14:48:00.475Z",
"formats": {
"javascript": {
"sources": 6,
"lines": 69,
"tokens": 861,
"clones": 3,
"duplicatedLines": 34,
"duplicatedTokens": 425,
"percentage": 49.275362318840585,
"percentageTokens": 49.361207897793264,
"newClones": 0,
"newDuplicatedLines": 0
}
},
"total": {
"sources": 9,
"lines": 192,
"tokens": 1813,
"clones": 3,
"duplicatedLines": 34,
"duplicatedTokens": 425,
"percentage": 17.708333333333336,
"percentageTokens": 23.44180915609487,
"newClones": 0,
"newDuplicatedLines": 0
}
}
}
jscpd writes the keys in alphabetical order; they are grouped here for reading, and formats has one entry per format in the scan.
| Key | Meaning |
|---|---|
format | The format of both fragments, named as jscpd --list names it |
kind | exact, renamed, similar or semantic (5.2.0+; semantic 5.3.3+) |
method | Which mechanism found a similar clone: gap for --max-gap-lines, ast for --similarity; absent for the other kinds (5.2.0+) |
similarity | For gap, the matched tokens over the merged span; for ast, the weighted Jaccard index of the two functions; for semantic, the cosine similarity of the embeddings; absent for exact and renamed |
isNew | true when the clone's fingerprint is missing from --baseline or --baseline-from-ref; false in a run without a baseline (5.1.0+) |
lines, tokens | The line span of the first fragment and the number of matched tokens |
firstFile, secondFile | name relative to the scanned directory (path:format for a block embedded in a Markdown, Vue, Svelte or Astro file), the start and end lines, and startLoc/endLoc with a 1-based line, a 0-based column and the byte position; with --blame, a blame object with commitSha and author |
fragment | The source of the first fragment |
statistics.total, statistics.formats.<format> | sources, lines, tokens, clones, duplicatedLines, duplicatedTokens, percentage (by lines), percentageTokens, newClones, newDuplicatedLines |
statistics.detectionDate | When the run finished, in ISO 8601 |
summary | Present only with --summary (5.0.16+) |
history | Present only with --history (5.2.1+) |
The language server (--lsp) answers its jscpd/clones request with the same clone entries, so an editor integration and a script read one shape.
Where it shows up
jq gets at the numbers directly. The duplication of the run, then the clones that are new against a baseline:
jq '.statistics.total.percentage' report/jscpd-report.json
17.708333333333336
jq -r '.duplicates[] | select(.isNew) | "\(.firstFile.name):\(.firstFile.start) ~ \(.secondFile.name):\(.secondFile.start) (\(.kind))"' report/jscpd-report.json
src/invoice.js:1 ~ src/print/invoice.js:1 (similar)
src/returns.js:1 ~ src/shipping.js:1 (renamed)
In GitHub Actions, keep the file as an artifact; if: always() keeps it when a gate fails the step:
- run: jscpd . -r console,json --output report --threshold 5
- uses: actions/upload-artifact@v4
if: always()
with:
name: jscpd-report
path: report/jscpd-report.json
With gates
--threshold leaves the file as it is: jscpd writes the report, then compares statistics.total.percentage with the threshold and exits 1 when it is above. A baseline marks each clone with isNew and counts them in newClones and newDuplicatedLines; --fail-on-new-clones turns that count into the exit code. --kind keeps only the kinds you name, in the clones and in the statistics.
Options
| Flag | Effect on the file |
|---|---|
-o, --output | The directory of jscpd-report.json |
-b, --blame | Adds blame to firstFile and secondFile |
--baseline, --baseline-from-ref | Sets isNew, newClones and newDuplicatedLines |
--summary, --history | Add the summary and history keys |
-a, --absolute | Absolute paths in reports |
Other modes
--compare writes jscpd-compare.json (Comparing two codebases documents it), --dashboard writes jscpd-dashboard.json, --health writes jscpd-health.json and --complexity writes jscpd-complexity.json, each with a shape of its own.
Related
- Clone types for what
kind,methodandsimilaritymean. - Baseline for
isNew. - Reporters for the other formats.