{"id":51743033,"url":"https://github.com/quicbit-js/qb-json-strict","last_synced_at":"2026-07-18T15:40:02.407Z","repository":{"id":367877118,"uuid":"1282667659","full_name":"quicbit-js/qb-json-strict","owner":"quicbit-js","description":"Opt-in RFC 8259 number/string content validation layer over qb-json-next","archived":false,"fork":false,"pushed_at":"2026-06-28T15:28:45.000Z","size":113,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-18T15:40:01.282Z","etag":null,"topics":["conformance","json","quicbit","rfc8259","tokenizer","utf8","validation"],"latest_commit_sha":null,"homepage":"https://github.com/quicbit-js/qb-json-next","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/quicbit-js.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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}},"created_at":"2026-06-28T04:15:56.000Z","updated_at":"2026-06-28T15:28:48.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/quicbit-js/qb-json-strict","commit_stats":null,"previous_names":["quicbit-js/qb-json-strict"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/quicbit-js/qb-json-strict","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quicbit-js%2Fqb-json-strict","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quicbit-js%2Fqb-json-strict/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quicbit-js%2Fqb-json-strict/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quicbit-js%2Fqb-json-strict/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/quicbit-js","download_url":"https://codeload.github.com/quicbit-js/qb-json-strict/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quicbit-js%2Fqb-json-strict/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35622753,"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-07-18T02:00:07.223Z","response_time":61,"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":["conformance","json","quicbit","rfc8259","tokenizer","utf8","validation"],"created_at":"2026-07-18T15:40:01.821Z","updated_at":"2026-07-18T15:40:02.387Z","avatar_url":"https://github.com/quicbit-js.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# qb-json-strict\n\nOpt-in **RFC 8259 content validation** layered over\n[qb-json-next](https://github.com/quicbit-js/qb-json-next).\n\n`qb-json-next` is a very fast incremental JSON **tokenizer**. It validates JSON *structure*\nrigorously (nesting, commas, colons, key/value placement, bracket/quote boundaries) but, to\nstay fast, deliberately does **not** validate the internal *content* of two token types:\n\n- **numbers** — it greedily consumes any run of `[-+0-9.eE]`, so `01`, `1.2.3`, `1e`, `1.`\n  pass through as a single decimal token.\n- **strings** — it only scans for the closing unescaped quote, so it accepts raw control\n  chars, bad escapes (`\\x`), and never checks UTF-8.\n\n`qb-json-strict` adds that missing layer as a thin wrapper, so full RFC 8259 conformance is\nopt-in. Code that wants raw scan speed keeps using `qb-json-next` directly and pays nothing.\n\n## How it compares\n\n**The fastest, smallest validating JSON tokenizer for JavaScript — and unlike `JSON.parse`,\nit streams.** Native `JSON.parse` is all-or-nothing: it needs the *whole* document up front and\na *single* invalid byte throws a `SyntaxError`, so you lose everything. Streaming tokenizers\nread incrementally (partial buffers, network chunks) and pinpoint *exactly where* a problem is\n— handing back everything parsed up to that point instead of failing the entire document.\nAmong them, qb is by far the fastest and the smallest to ship.\n\n| library | role | MB/s | minified | gzip | streaming | on malformed JSON |\n|---|---|--:|--:|--:|:--:|---|\n| `JSON.parse` (native, C++) | parse → value tree | ~700 | 0 (built-in) | — | ✗ | throws — **entire document lost** |\n| qb-json-next | tokenizer (structure) | 640 | 5.2 KB | 2.0 KB | ✓ | exact byte offset; tokens before it kept |\n| **qb-json-strict** | **validating tokenizer** | **420** | **8.0 KB** | **3.0 KB** | ✓ | exact offset **+ reason**; tokens before it kept |\n| clarinet | SAX parser | 135 | 161.9 KB | 51.4 KB | ✓ | error event with position |\n| jsonparse | streaming parser | 98 | 35.6 KB | 10.7 KB | ✓ | error with position |\n| @streamparser/json | tokenizer / parser | 84 / 70 | 23.9 KB | 5.6 KB | ✓ | error with position |\n\n\u003csub\u003eSorted fastest → slowest. 64 MB of representative JSON, Apple M2 Pro / Node 22. **Minified**\nis the JavaScript that ships to a browser and gets parsed and run — what actually costs you;\ngzip is only the network-transfer cost. qb has no dependencies or polyfills (just\nits own minified code); jsonparse and clarinet declare zero deps but assume Node's `Buffer`/`Stream`,\nso the polyfills they need in a browser are included. See [Performance](#performance) to reproduce.\u003c/sub\u003e\n\nAmong pure-JavaScript libraries qb-json-strict does the **most** work (full RFC 8259 content\nvalidation) yet runs **~3–5× faster** than the other streaming parsers at just **~8 KB\nminified** — ~20× smaller than clarinet, ~4× smaller than jsonparse. Reach for `JSON.parse` when you\nhave a complete, well-formed document and just want a value (it's faster and free); reach for qb\nwhen you need streaming, partial-buffer, or fault-tolerant parsing — with strict validation.\n\n## Install\n\n```\nnpm install qb-json-strict\n```\n\n## Usage\n\n### Validate a whole document (one-shot)\n\n```js\nconst { validate } = require('qb-json-strict')\n\nvalidate('{\"a\":[1,-2.5e3],\"b\":\"café 😀\"}')   // -\u003e null  (valid)\nvalidate('{\"a\":01}')                          // -\u003e 'number: leading zero at 6..8'\nvalidate('\"bad \\\\x escape\"')                  // -\u003e 'string: bad escape at ...'\nvalidate('[1,2')                              // -\u003e 'unclosed container at ...'\n```\n\n`validate(src)` accepts a `Buffer` or string and returns `null` when `src` is a single,\ncomplete, conformant JSON document, otherwise a short error message. On top of per-token\ncontent checking it also enforces the document-level rules that `qb-json-next`'s lenient\n*streaming* tokenizer relaxes: exactly one top-level value, no stray/unbalanced closing\nbrackets, and only space/tab/LF/CR as whitespace (the tokenizer also tolerates `\\b` and `\\f`).\n\n### Incremental, drop-in for next()\n\n```js\nconst strict = require('qb-json-strict')\n\nconst ps = strict.ps(Buffer.from('{\"a\": \"value\", \"b\": 1.5}'))\nwhile (strict.next_strict(ps)) {\n  // same ps, same offsets, same tokens as qb-json-next's next()...\n  // but number/string/key content is additionally validated to RFC 8259.\n}\n```\n\n`next_strict(ps, opt)` is a drop-in for `next(ps, opt)`: same parse-state object, same\noffsets, same token codes. It additionally validates the *content* of number tokens, string\ntokens, and object keys (which are strings). On non-conformant content it follows the same\nerror convention as `next()`: it sets `ps.tok = 0` and `ps.ecode = BAD_VALUE` (sticky), then\ncalls `opt.err(ps)` if provided, else throws an `Error` carrying `.parse_state`.\n\nNote: `next_strict` is the per-token content layer only. Document-level strictness (single\ntop-level value, balanced brackets, legal whitespace) lives in `validate()`, because the\nunderlying tokenizer is intentionally a lenient multi-value stream tokenizer.\n\n## API\n\n- `validate(src)` → `null` if valid, else an error message string. `src` is a `Buffer` or string.\n- `next_strict(ps, opt)` → drop-in for `next()` that additionally content-validates d/s tokens and keys.\n- `check_number(src, off, lim)` → `null` if `src[off..lim]` is a strict RFC 8259 number, else a message.\n- `check_string(src, off, lim)` → `null` if `src[off..lim]` (quotes included) is a strict RFC 8259 string, else a message.\n- Re-exports from `qb-json-next`: `ps`, `next`, `tokstr`, `TOK`, `ECODE`, `POS`.\n\n### Number grammar (check_number)\n\n```\nnumber = [ \"-\" ] int [ frac ] [ exp ]\nint    = \"0\" / ( digit1-9 *DIGIT )      ; no leading zeros\nfrac   = \".\" 1*DIGIT\nexp    = (\"e\" / \"E\") [ \"+\" / \"-\" ] 1*DIGIT\n```\n\nRejects `01`, `1.`, `.5`, `+5`, `1e`, `1e+`, `--5`, `1.2.3`. Accepts `0`, `-0`, `123`,\n`1.5`, `-1.5e10`, `1E-9`, `0.0`. Numeric *magnitude/precision* is not range-checked — that is\nthe consumer's concern; `check_number` validates grammar only.\n\n### String rules (check_string)\n\nOffsets **include the surrounding quotes** (qb-json-next convention), so the interior bytes\nare `off+1 .. lim-2`.\n\n- Rejects raw control chars `0x00`–`0x1F` (must be escaped). `0x7F` (DEL) is allowed per RFC.\n- `\\` must be followed by one of `\" \\ / b f n r t u`; `\\u` must be followed by exactly 4 hex digits.\n- Validates UTF-8 well-formedness: rejects overlong encodings, lone/raw surrogates\n  (`U+D800`–`U+DFFF`), bytes `\u003e U+10FFFF`, stray continuation bytes, and truncated sequences.\n\n## Conformance\n\nTested against the [JSONTestSuite](https://github.com/nst/JSONTestSuite) `test_parsing`\ncorpus: **95/95** `y_` (must-accept) and **188/188** `n_` (must-reject) pass.\n\nImplementation-defined (`i_`) choices:\n\n- **Number magnitude/precision** (huge exponents, overflow, underflow) → **accepted** (grammar only).\n- **Lone/invalid surrogates inside `\\u` escapes** (e.g. `\"\\uD800\"`) → **accepted** (grammar-valid hex).\n- **Raw UTF-8 surrogates, overlong sequences, out-of-range bytes** → **rejected** (strict UTF-8).\n- **UTF-16 / byte-order marks** → **rejected** (input must be UTF-8).\n- **Deeply nested structures** (e.g. 500 levels) → **accepted** (no depth limit).\n\n## Performance\n\nRun `npm run perf` (replicates a sample into a large buffer and tokenizes it five ways).\nOn an Apple M2 Pro (node 22), tokenizing 64 MB of representative JSON:\n\n| mode                          | MB/s | vs raw |\n|-------------------------------|-----:|-------:|\n| bare byte scan (ceiling)      | 1095 |   162% |\n| `qb-json-next` `next()` (raw) |  675 |   100% |\n| `next_strict()` (validating)  |  420 |    62% |\n| `validate()` (one-shot)       |  414 |    61% |\n| `JSON.parse` (reference)      |  652 |    97% |\n\n**The strictness layer costs ~38% throughput** (~675 → ~420 MB/s). The cost is roughly\n\"scan every value byte a second time\": isolating each checker on type-homogeneous data,\nnumber validation costs ~44% and string validation ~46% — about the same, since both are a\nsecond per-token pass on top of the tokenizer's own scan. This is exactly why it is opt-in:\nsearch/scan use cases that don't need content validation keep the full raw speed.\n\n### Compared to other JS libraries\n\nSee [How it compares](#how-it-compares) at the top for the speed + bundle-size table.\n\nOn footprint: beware \"zero dependencies\". clarinet and jsonparse declare none, but they assume\nNode's built-in `Buffer`/`Stream`, which a browser must polyfill — and the Stream polyfill\n(`readable-stream` \u0026 friends) is large, which is why clarinet really costs **~162 KB minified**\n(~51 KB gzipped). qb and `@streamparser/json` use no Node APIs, so what you see is what you ship.\n\nReproduce (the tooling and compared libraries are optional, not dependencies):\n\n```\nnpm install --no-save clarinet jsonparse @streamparser/json\nnpm run compare       # throughput\nnpm install --no-save esbuild esbuild-plugin-polyfill-node clarinet jsonparse @streamparser/json\nnpm run size          # browser bundle footprint\n```\n\n## Tests\n\n```\nnpm test          # unit tables (self-contained)\nnpm run test:suite  # JSONTestSuite corpus (see below)\nnpm run test:all  # both\n```\n\nThe corpus is not vendored. Fetch it once:\n\n```\ngit clone --depth 1 https://github.com/nst/JSONTestSuite.git\n```\n\n`test-suite.js` finds it at `./JSONTestSuite`, `../JSONTestSuite`, or `$JSONTESTSUITE_DIR`.\n\n## License\n\nISC\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fquicbit-js%2Fqb-json-strict","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fquicbit-js%2Fqb-json-strict","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fquicbit-js%2Fqb-json-strict/lists"}