Reporters

JSON reporter

Every clone with its locations, kind and source, plus the statistics per format, in one file for scripts and pipelines.

-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

Terminal
jscpd . -r json --output report
JSON report saved to report/jscpd-report.json
.jscpd.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:

report/jscpd-report.json
{
  "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.

KeyMeaning
formatThe format of both fragments, named as jscpd --list names it
kindexact, renamed, similar or semantic (5.2.0+; semantic 5.3.3+)
methodWhich mechanism found a similar clone: gap for --max-gap-lines, ast for --similarity; absent for the other kinds (5.2.0+)
similarityFor 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
isNewtrue when the clone's fingerprint is missing from --baseline or --baseline-from-ref; false in a run without a baseline (5.1.0+)
lines, tokensThe line span of the first fragment and the number of matched tokens
firstFile, secondFilename 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
fragmentThe source of the first fragment
statistics.total, statistics.formats.<format>sources, lines, tokens, clones, duplicatedLines, duplicatedTokens, percentage (by lines), percentageTokens, newClones, newDuplicatedLines
statistics.detectionDateWhen the run finished, in ISO 8601
summaryPresent only with --summary (5.0.16+)
historyPresent 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:

Terminal
jq '.statistics.total.percentage' report/jscpd-report.json
17.708333333333336
Terminal
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:

.github/workflows/jscpd.yml
- 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

FlagEffect on the file
-o, --outputThe directory of jscpd-report.json
-b, --blameAdds blame to firstFile and secondFile
--baseline, --baseline-from-refSets isNew, newClones and newDuplicatedLines
--summary, --historyAdd the summary and history keys
-a, --absoluteAbsolute 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.