{"slug": "show-hn-json-to-md-gfm", "title": "Show HN: JSON to Md(gfm)", "summary": "Developer Raj Nandan released json-to-md, an open-source tool that converts JSON documents into deterministic, human-readable GitHub Flavored Markdown, with byte-identical output across TypeScript and Go implementations. The tool aims to reduce token costs when feeding data into large language models by replacing JSON punctuation with Markdown headings and lists. It ships as an npm package and a Go module with no runtime dependencies, supporting browser, Node, and command-line use.", "body_md": "Convert one JSON document into deterministic, human-readable [GitHub Flavored Markdown](https://github.github.com/gfm/).\n\nThe same conversion runs in the browser, in Node, in Go, and on the command line. The TypeScript and Go implementations produce **byte-identical output**, enforced by a shared [ corpus/](corpus/) and a cross-implementation fuzz gate in CI. Output is a readable\n\n`# Results`\n\nheading and uses canonical spacing: LF endings, one blank line between blocks, no trailing spaces, one final newline.If you feed JSON into an LLM, you pay for its punctuation. Every `{`\n\n, `}`\n\n, `\"`\n\n,\n`:`\n\n, and `,`\n\nis tokens spent on structure the model does not need to read the\ndata. Converting the same document to Markdown headings and lists is measurably\ncheaper and easier for models to follow:\n\nThe tradeoff: this is a one-way, human-facing **projection**, not a reversible\nserialization. When a downstream step must parse, validate, or store the data,\nkeep the JSON. A common pattern is JSON for the machine contract and Markdown\nfor everything a model or a person has to read.\n\n```\nnpm install @rajnandan1/json-to-md      # or: pnpm add / yarn add\n```\n\nShips ESM and CommonJS builds with TypeScript declarations, and no runtime dependencies. Node ≥ 18.\n\n``` js\nimport { convertJsonText, convertJsonValue } from \"@rajnandan1/json-to-md\";\n\nconvertJsonText('{ \"hello\": \"world\" }'); // untrusted serialized JSON text\nconvertJsonValue({ hello: \"world\" }); // already-parsed, caller-trusted data\n// # Results\n//\n// ## hello\n//\n// world\n```\n\nCommonJS works too: `const { convertJsonText } = require(\"@rajnandan1/json-to-md\")`\n\n. See the [API reference](#api-reference) for the two entry points’ exact contracts and errors.\n\nThe same package: bundle it (Vite, webpack, esbuild; it’s plain ESM with no dependencies), or skip the build step entirely. Every npm release lands on the CDNs, and the build is a single self-contained ES module, so the raw file imports directly:\n\n``` js\n<script type=\"module\">\n    import { convertJsonText } from \"https://cdn.jsdelivr.net/npm/@rajnandan1/json-to-md@1/dist/index.js\";\n    document.body.textContent = convertJsonText('{\"hello\":\"world\"}');\n</script>\n```\n\n(`@1`\n\nfloats on the latest 1.x; pin `@1.0.1`\n\nfor exact bytes. The same path works on unpkg at `https://unpkg.com/@rajnandan1/json-to-md@1.0.1/dist/index.js`\n\n, or via esm.sh as `https://esm.sh/@rajnandan1/json-to-md`\n\n.)\n\nNo modules? A classic script tag works too; the IIFE build exposes a `jsonToMd`\n\nglobal. Pin it with [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) so a compromised CDN can’t swap the code (the hash is per-version, so update both together when bumping):\n\n```\n<script\n    src=\"https://cdn.jsdelivr.net/npm/@rajnandan1/json-to-md@1.0.1/dist/index.global.js\"\n    integrity=\"sha384-v5+v71wtLTKJo2nT4Ly2Ov4u1QDPPPC0fhwy1q9I8lqsEtDOIuH/4AIwyt9lRQmR\"\n    crossorigin=\"anonymous\"\n></script>\n<script>\n    document.body.textContent = jsonToMd.convertJsonText('{\"hello\":\"world\"}');\n</script>\n```\n\nIdentical API to Node; the conversion core is pure and runs anywhere. A live playground lives in [ demo/](https://json-to-md.rajnandan.com/demo/), a fully static page that loads the released library from the CDN, so any static file server works:\n\n```\npnpm demo   # serves it via vite and prints a localhost URL\n```\n\nPaste JSON, watch it convert, toggle between the rendered preview and raw Markdown, and switch the input between the serialized and parsed entry points to see numeric spelling preserved or lost.\n\n```\ngo get github.com/rajnandan1/json-to-md/go\nimport (\n\t\"errors\"\n\n\tjsontomd \"github.com/rajnandan1/json-to-md/go\"\n)\n\nmd, err := jsontomd.ConvertText([]byte(`{\"hello\":\"world\"}`)) // byte-identical to convertJsonText\nmd, err = jsontomd.ConvertValue(v)                           // any Go value, marshal-then-convert\n\nvar convErr *jsontomd.Error\nif errors.As(err, &convErr) {\n\t// convErr.Code, .Pointer, .Location: same codes and UTF-16 locations as the TS errors\n}\n```\n\n`ConvertText`\n\npreserves member order and numeric lexemes just like `convertJsonText`\n\n. `ConvertValue`\n\nis defined as `json.Marshal(v)`\n\npiped into the same core: struct fields render in field order, map keys in sorted order, and the core rejects cycles or NaN. The library packages are dependency-free.\n\n```\nbrew tap rajnandan1/homebrew-rajnandan\nbrew trust rajnandan1/rajnandan     # once per machine; newer Homebrew gates third-party taps\nbrew install json-to-md\n# or, via the Go toolchain:\ngo install github.com/rajnandan1/json-to-md/go/cmd/json-to-md@latest\njson-to-md data.json > out.md                       # file in, Markdown out\ncurl -s https://api.example.com/items | json-to-md  # stdin (no arg, or '-')\njson-to-md --json broken.json 2> error.json         # structured errors on stderr\njson-to-md --version\n```\n\nExit codes: `0`\n\nsuccess, `1`\n\nconversion failed, `2`\n\nusage or I/O error. Failures print one greppable line (`json-to-md: DUPLICATE_MEMBER_NAME at 3:7 (first at 1:9): …`\n\n), or with `--json`\n\nthe full error object (`code`\n\n, `pointer`\n\n, `location`\n\n, `firstLocation`\n\n, `message`\n\n).\n\nThere are two entry points, one for already-parsed data and one for untrusted JSON text, plus a typed error. Both functions return the complete Markdown string or throw `JsonToMarkdownError`\n\n. Calls are pure, never mutate input, share no state, and are safe to run concurrently.\n\nGo mirrors these contracts one-for-one: `ConvertText`\n\n↔ `convertJsonText`\n\n(byte-identical output), `ConvertValue`\n\n↔ `convertJsonValue`\n\n(host-language marshaling semantics), and `*jsontomd.Error`\n\n↔ `JsonToMarkdownError`\n\n, with the same codes, JSON Pointers, and UTF-16 locations (`SPARSE_ARRAY`\n\ncannot occur in Go).\n\n`convertJsonValue(value: unknown): string`\n\nAccepts caller-trusted, already-parsed JSON data and validates it at runtime. Rejects anything that is not JSON-compatible: cycles, sparse arrays, `undefined`\n\n, `BigInt`\n\n, `NaN`\n\n/`Infinity`\n\n, functions, symbols, `Date`\n\n/`Map`\n\n/`Set`\n\n, and class instances. It never invokes getters, so it cannot detect a hostile `Proxy`\n\n; put untrusted content in `convertJsonText`\n\n.\n\n`convertJsonText(source: string): string`\n\nAccepts untrusted **serialized** JSON, parses it without executing caller code, and preserves each number’s original spelling. It keeps object-member order, array order, and numeric tokens intact.\n\n```\nconvertJsonText(\"9007199254740993\"); // \"…\\n\\n9007199254740993\\n\"  (preserved)\nconvertJsonValue(9007199254740993); // \"…\\n\\n9007199254740992\\n\"  (already rounded by JS)\n\nconvertJsonText(\"1.00\"); // \"1.00\"\nconvertJsonValue(1.0); // \"1\"\ntry {\n    convertJsonText('{\"name\":\"a\",\"name\":\"b\"}');\n} catch (e) {\n    if (e instanceof JsonToMarkdownError) {\n        e.code; // \"DUPLICATE_MEMBER_NAME\"\n        e.message; // human-readable\n        e.location; // { offset, line, column } for serialized syntax failures\n        e.firstLocation; // first occurrence, for duplicate member names\n        e.pointer; // JSON Pointer, when the invalid value is locatable\n    }\n}\n```\n\nConversion fails atomically at the first error in encounter order and never returns partial Markdown. Codes: `INVALID_JSON_SYNTAX`\n\n, `DUPLICATE_MEMBER_NAME`\n\n, `INVALID_PARSED_VALUE`\n\n, `CYCLIC_REFERENCE`\n\n, `SPARSE_ARRAY`\n\n.\n\n| Input | Rendering |\n|---|---|\n| Object keys | Headings H2–H6, then nested unordered lists once nesting passes H6 |\n| Array of objects (a Tabular Array) | One GFM table; also when nested inside a list |\n| Any other array | Unordered list in source order |\n| Non-empty container in a table cell | Link to a Detail Section headed by the value’s JSON Pointer, preceded by a `---` thematic break |\n| String | Escaped literal text; never injects Markdown or HTML |\nWhole-string absolute `http(s)` URL |\nMarkdown link |\n`null` / `\"\"` / `[]` / `{}` |\n``null`` / ``\"\"`` / ``[]`` / ``{}`` (each kept distinct from a missing table cell) |\n\n```\nconvertJsonText(\n    JSON.stringify({\n        table1: [{ age: 14, degrees: [{ name: \"B-Degree\", year: \"2023\" }] }],\n    }),\n);\n# Results\n\n## table1\n\n| age | degrees                              |\n| --- | ------------------------------------ |\n| 14  | [/table1/0/degrees](#table10degrees) |\n\n---\n\n### /table1/0/degrees\n\n| name     | year |\n| -------- | ---- |\n| B-Degree | 2023 |\n```\n\nThere is no converter-defined input-size, nesting-depth, or table-size limit; parsing, validation, and rendering are iterative, so deeply nested documents do not overflow the stack.\n\nMeasured on Stripe’s OpenAPI spec ([spec3.json](https://github.com/stripe/openapi/blob/master/openapi/spec3.json): 7.5 MiB of JSON in, 6.1 MiB of Markdown out), Apple M3 Pro, 2026-07. Both implementations produced **byte-identical output** for this document (sha256-verified). The corpus promise holds on real-world data.\n\n| Surface | Median | Input throughput |\n|---|---|---|\nGo `ConvertText` |\n128 ms | ~59 MiB/s |\nTypeScript `convertJsonText` (Node 24) |\n211 ms | ~36 MiB/s |\nCLI `json-to-md spec3.json` (whole process, brew binary) |\n~120 ms | — |\n\nReproduce with any large JSON document:\n\n```\npnpm build && node scripts/bench-file.mjs path/to/big.json\ncd go && BENCH_FILE=path/to/big.json go test -bench ConvertTextFile -run '^$'\npnpm build       # ESM + CJS + type declarations (tsup)\npnpm typecheck   # tsc --noEmit\npnpm test        # vitest run\npnpm bench       # 10 MiB conversion benchmarks\n```\n\nMIT", "url": "https://wpnews.pro/news/show-hn-json-to-md-gfm", "canonical_source": "https://json-to-md.rajnandan.com/", "published_at": "2026-07-22 17:42:55+00:00", "updated_at": "2026-07-22 17:52:13.610245+00:00", "lang": "en", "topics": ["developer-tools", "large-language-models"], "entities": ["Raj Nandan", "json-to-md", "GitHub Flavored Markdown", "TypeScript", "Go", "npm", "Node.js"], "alternates": {"html": "https://wpnews.pro/news/show-hn-json-to-md-gfm", "markdown": "https://wpnews.pro/news/show-hn-json-to-md-gfm.md", "text": "https://wpnews.pro/news/show-hn-json-to-md-gfm.txt", "jsonld": "https://wpnews.pro/news/show-hn-json-to-md-gfm.jsonld"}}