{"id":50708451,"url":"https://github.com/privatenumber/doctor-json","last_synced_at":"2026-06-09T13:31:52.331Z","repository":{"id":343233943,"uuid":"1170094422","full_name":"privatenumber/doctor-json","owner":"privatenumber","description":"Surgically edit JSON \u0026 JSONC strings while preserving whitespace, comments, and formatting","archived":false,"fork":false,"pushed_at":"2026-03-01T18:07:38.000Z","size":115,"stargazers_count":25,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"develop","last_synced_at":"2026-05-06T11:49:12.847Z","etag":null,"topics":["ast","comments","edit","json","jsonc","preserve","whitespace"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/privatenumber.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"privatenumber"}},"created_at":"2026-03-01T17:34:56.000Z","updated_at":"2026-04-30T21:57:43.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/privatenumber/doctor-json","commit_stats":null,"previous_names":["privatenumber/doctor-json"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/privatenumber/doctor-json","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/privatenumber%2Fdoctor-json","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/privatenumber%2Fdoctor-json/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/privatenumber%2Fdoctor-json/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/privatenumber%2Fdoctor-json/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/privatenumber","download_url":"https://codeload.github.com/privatenumber/doctor-json/tar.gz/refs/heads/develop","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/privatenumber%2Fdoctor-json/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34110011,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-09T02:00:06.510Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ast","comments","edit","json","jsonc","preserve","whitespace"],"created_at":"2026-06-09T13:31:52.044Z","updated_at":"2026-06-09T13:31:52.321Z","avatar_url":"https://github.com/privatenumber.png","language":"TypeScript","funding_links":["https://github.com/sponsors/privatenumber"],"categories":[],"sub_categories":[],"readme":"# doctor-json\n\nSurgically edit JSON \u0026 JSONC strings — preserving whitespace, comments, and formatting.\n\n## Why?\n\nJSON files have formatting that matters — comments, indentation style, trailing commas, hand-organized sections. `JSON.parse` + `JSON.stringify` destroys all of it.\n\n_Doctor JSON_ lets you edit JSON like a normal object. When you stringify, only what you changed is different. Everything else is byte-identical.\n\n```ts\nimport { parse, stringify } from 'doctor-json'\n\nconst config = parse(tsconfigText)\nconfig.compilerOptions.target = 'ES2024'\n\nawait fs.writeFile('tsconfig.json', stringify(config))\n```\n\nWith `JSON.stringify`, one field change destroys the entire file:\n\n```json\n{\"compilerOptions\":{\"target\":\"ES2024\",\"strict\":true},\"include\":[\"src\"]}\n```\n\nWith _Doctor JSON_, only the value you touched is different:\n\n```jsonc\n{\n  // Compiler options\n  \"compilerOptions\": {\n    \"target\": \"ES2024\", // latest stable\n    \"strict\": true,\n  },\n  \"include\": [\"src\"]\n}\n```\n\n## Install\n\n```sh\nnpm install doctor-json\n```\n\n## Usage\n\n`parse()` returns a plain JavaScript object. Mutate it with standard JS. `stringify()` diffs your changes against the original and patches the text.\n\n```ts\nimport { parse, stringify, sortKeys, rename } from 'doctor-json'\n\nconst pkg = parse(text)\n\npkg.version = '2.0.0'\npkg.keywords.push('json', 'ast')\ndelete pkg.deprecated\n\nsortKeys(pkg.dependencies)\nrename(pkg.scripts, 'build', 'compile')\n\nconst result = stringify(pkg)\n```\n\nNo proxies, no special APIs. `Array.isArray`, `Object.keys`, `for...of`, spread, destructuring — everything works natively because it's a real object.\n\n## Examples\n\n### Edit a package.json with formatting\n\nReal package.json files often have tabs, blank-line section separators, and `\"// comment\"` keys as comment workarounds. _Doctor JSON_ preserves all of it:\n\n```ts\nimport { parse, stringify, sortKeys, rename } from 'doctor-json'\n\nconst pkg = parse(packageJsonText)\n\npkg.version = '2.0.0'\nrename(pkg.scripts, 'build', 'compile')\npkg.dependencies.pinia = '^2.1.0'\nsortKeys(pkg.dependencies)\n\nawait fs.writeFile('package.json', stringify(pkg))\n// Tabs, blank-line groups, \"// comment\" keys — all preserved\n```\n\n\u003e See [examples/package-json.ts](examples/package-json.ts) for the full before/after with tabs, grouped sections, and comment keys.\n\n### Update a tsconfig.json (JSONC)\n\nComments and trailing commas survive all operations:\n\n```ts\nconst config = parse(tsconfigText)\nconfig.compilerOptions.target = 'ES2024'\nconfig.compilerOptions.noUncheckedIndexedAccess = true\nconfig.exclude.push('coverage')\n// Line comments, block comments, trailing commas — all preserved\n```\n\n\u003e See [examples/tsconfig-jsonc.ts](examples/tsconfig-jsonc.ts) for a full JSONC editing example.\n\n### Rename a key (preserving position and comments)\n\n`rename` changes the key name without moving it or losing its surrounding formatting:\n\n```jsonc\n// Before\n{\n  \"scripts\": {\n    // Compile TypeScript\n    \"build\": \"tsc\",\n    \"test\": \"vitest\"\n  }\n}\n```\n\n```ts\nrename(pkg.scripts, 'build', 'compile')\n```\n\n```jsonc\n// After\n{\n  \"scripts\": {\n    // Compile TypeScript\n    \"compile\": \"tsc\",\n    \"test\": \"vitest\"\n  }\n}\n```\n\nOnly the key name changed. The comment, value, and position are all preserved. With `delete` + re-add, the key moves to the end and the comment is lost.\n\n### More examples\n\n- [Bulk update with Object.assign](examples/bulk-update.ts)\n- [Array manipulation (splice, push, sort)](examples/array-manipulation.ts)\n\n## API\n\n```ts\nimport { parse, stringify, sortKeys, rename } from 'doctor-json'\n```\n\n### `parse(text)`\n\nParse a JSON/JSONC string. Returns a plain JavaScript object.\n\n### `stringify(obj)`\n\nProduce the edited text. Unchanged content keeps its original formatting, comments, and whitespace.\n\n```ts\nconst result = stringify(pkg)\nawait fs.writeFile('package.json', result)\n```\n\n### `sortKeys(obj, comparator?)`\n\nSort object keys. Comments travel with their keys. Blank lines between members create independent [sort groups](#sort-groups) — members never cross group boundaries.\n\n```ts\nsortKeys(pkg.dependencies)                // alphabetical\nsortKeys(pkg, (a, b) =\u003e customOrder(a, b)) // custom comparator\n```\n\n### `rename(obj, oldKey, newKey)`\n\nRename a key in place. Position, value, and surrounding comments are preserved.\n\n```ts\nrename(pkg.scripts, 'build', 'compile')\n```\n\n## Behavior\n\n### Formatting preservation\n\n_Doctor JSON_ detects formatting per-object — indentation style, colon spacing, inline vs multiline, trailing commas. New content matches the style of the object it's inserted into.\n\n```ts\n// Minified input → minified output\nparse('{\"a\":1}').b = 2     // → '{\"a\":1,\"b\":2}'\n\n// 4-space indent → 4-space output\nparse('{\\n    \"a\": 1\\n}')  // new keys get 4-space indent\n```\n\n### JSONC support\n\nComments and trailing commas are preserved through all operations, including comments between key and value:\n\n```ts\nconst config = parse('{\"key\": /* important */ \"old\"}')\nconfig.key = 'new'\nstringify(config) // '{\"key\": /* important */ \"new\"}'\n```\n\n### Comment association\n\nWhen sorting or removing members, comments travel with their associated member:\n\n- **Same-line comments** (`// note` after a value) stay with that member\n- **Above-line comments** (comment on the line above) stay with the member below\n\nTo pin a comment as a section header that doesn't move during sort, separate it with a blank line.\n\n### Sort groups\n\nBlank lines between members create independent sort groups. `sortKeys` sorts within each group but never moves members across group boundaries:\n\n```jsonc\n{\n  // These two sort together\n  \"b\": 1,\n  \"a\": 2,\n\n  // These two sort together (separately)\n  \"d\": 3,\n  \"c\": 4\n}\n```\n\nAfter `sortKeys`: `a, b` in group 1, `c, d` in group 2. The blank line keeps them apart.\n\n## Notes\n\n- `stringify(pkg)` is the surgical output. `JSON.stringify(pkg)` re-serializes from scratch (comments and formatting lost).\n- `parse()` returns plain objects with normal prototypes — `instanceof Object`, `hasOwnProperty`, and `toString` all work.\n- Duplicate keys use last-key-wins (matching `JSON.parse`).\n- Value coercion follows `JSON.stringify` semantics — `Date` calls `toJSON()`, `undefined`/functions are omitted, `NaN`/`Infinity` become `null`.\n\n## How it works\n\n```\n1. parse(text)\n   ├─ Parse text → AST (preserves comments, whitespace)\n   ├─ Evaluate AST → plain JS object\n   └─ Snapshot the object state\n\n2. Mutate with normal JS\n   obj.key = 'new value'\n\n3. stringify(obj)\n   ├─ Diff current object vs snapshot → find what changed\n   ├─ Patch only the changed parts in the original text\n   └─ Return the patched text\n```\n\nUnchanged text is never touched, so formatting, comments, and whitespace survive.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fprivatenumber%2Fdoctor-json","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fprivatenumber%2Fdoctor-json","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fprivatenumber%2Fdoctor-json/lists"}