jsonfix 0.1.0

Repair, extract, and partially parse malformed JSON from LLMs and other unreliable sources. no_std, zero dependencies, lossless numbers.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
# jsonfix

[![Crates.io](https://img.shields.io/crates/v/jsonfix)](https://crates.io/crates/jsonfix)
[![Documentation](https://docs.rs/jsonfix/badge.svg)](https://docs.rs/jsonfix)
[![License](https://img.shields.io/crates/l/jsonfix)](#license)
[![CI](https://img.shields.io/github/actions/workflow/status/themankindproject/jsonfix/ci.yml?branch=main&label=CI)](https://github.com/themankindproject/jsonfix/actions/workflows/ci.yml)
![Crates.io Downloads](https://img.shields.io/crates/d/jsonfix)
![Rust Version](https://img.shields.io/badge/rust-1.85%2B-blue)

`jsonfix` repairs, extracts, and parses the JSON that language models, logs,
and hand-edited files *almost* produce: trailing commas, `'single quotes'`,
markdown fences, prose around the document, `None` instead of `null`, or a
stream cut off mid-token. One call turns it into valid JSON — or into the best
prefix of it — and tells you byte-exactly what it could not save.

Zero dependencies, `#![no_std]` + `alloc`, `#![forbid(unsafe_code)]`, lossless
numbers. Modeled on the excellent JavaScript
[`jsonrepair`](https://github.com/josdejong/jsonrepair) library.

## Install

```toml
[dependencies]
jsonfix = "0.1"
```

| Feature | Adds | Default |
|---|---|---|
| *(none)* | repair, extract, parse, stream | ✔ |
| `std` | `repair_to_writer` (`io::Write` sinks) | — |
| `serde` | `from_value`: `Value` → your own types | — |
| `serde_json` | `loads` / `deserialize` (implies `serde`) | — |

Works with `default-features = false` — the crate needs only `alloc`.

## The five verbs

| Function | Use it for |
|---|---|
| [`repair`](#repairing) | turn broken JSON into a valid `String` |
| [`extract`](#finding-json-in-prose) | pull the JSON span out of prose or a fence |
| [`parse`](#parsing-to-a-tree) / [`parse_partial`](#partial-parsing-and-truncated-input) | get a `Value` tree, complete or still streaming |
| [`StreamRepairer`](#streaming-token-by-token) | repair a document that arrives chunk by chunk |
| [`validate`](#choosing-what-gets-repaired) | strict check with byte-precise errors |

```rust
use jsonfix::{extract, parse, repair, repair_extract};

let reply = "Sure! Here you go:\n```json\n{name: 'Ada', age: 36,}\n```";
// A chat reply has prose around the JSON: extract first, then repair.
assert_eq!(extract(reply), Some("{name: 'Ada', age: 36,}"));
assert_eq!(repair_extract(reply).unwrap(), r#"{"name": "Ada", "age": 36}"#);
// Without prose the same functions work on the bare document.
assert_eq!(repair("{name: 'Ada'}").unwrap(), r#"{"name": "Ada"}"#);
assert_eq!(parse("{age: 36}").unwrap().get("age").and_then(|v| v.as_i64()), Some(36));
```

## Repairing

`repair` returns canonical JSON text; `repair_with` takes [`Options`](#choosing-what-gets-repaired).
All of the damage below is repaired by default (each is a toggleable pass):

| Damage | Input | Output |
|---|---|---|
| unquoted keys/values | `{name: 'Ada'}` | `{"name": "Ada"}` |
| trailing / missing commas | `[1, 2,]` | `[1, 2]` |
| comments | `{"a": 1 /* note */}` | `{"a": 1}` |
| `True`/`False`/`None`/`undefined` | `{a: True, b: None}` | `{"a": true, "b": null}` |
| `NaN`/`Infinity` | `{"a": NaN}` | `{"a": "NaN"}` |
| numeric oddities | `[.5, 2., 2e, -, 01]` | `[0.5, 2.0, 2e0, -0, "01"]` |
| typographic quotes | `{"a": “v”}` | `{"a": "v"}` |
| missing escapes | `{"a": "x"y"z"}` | `{"a": "x\"y\"z"}` |
| string concatenation | `{"a": "x" + "y"}` | `{"a": "xy"}` |
| wrapper calls | `NumberLong(2)`, `ISODate("d")` | `2`, `"d"` |
| ellipses | `[1, 2, ...]` | `[1, 2]` |
| truncation | `{"a": [1, 2` | `{"a": [1, 2]}` |
| NDJSON / several values | `1\n2\n3` | `[1, 2, 3]` |
| markdown fences + prose | ```` ```json\n{...}\n``` ```` | the document |
| JSONP | `cb({"a": 1})` | `{"a": 1}` |
| one escaping layer too many | `{\"a\": 1}` | `{"a": 1}` |

```rust
use jsonfix::repair;

assert_eq!(repair("{a: 1, /* note */ b: 'two',}").unwrap(), r#"{"a": 1, "b": "two"}"#);
assert_eq!(repair("{\"a\": [1, 2").unwrap(), r#"{"a": [1, 2]}"#);
assert_eq!(repair("1\n2\n3").unwrap(), "[1, 2, 3]");
```

## Finding JSON in prose

`extract` returns the raw span (unrepaired) of the first value; `extract_all`
returns every top-level span; `extract_partial` returns from the first value to
the end of input (useful while a stream is still arriving). `repair_extract` is
the convenience combo: extract, repair, and fall back to repairing the whole
input if the span is not JSON.

```rust
use jsonfix::{extract, extract_all, extract_partial, repair_extract};

assert_eq!(extract(r#"The result is {"ok": true,} — done."#), Some(r#"{"ok": true,}"#));
assert_eq!(extract("no json here"), None);
assert_eq!(extract_all("{\"a\": 1}\n{\"b\": 2}"), ["{\"a\": 1}", "{\"b\": 2}"]);
assert_eq!(extract_partial(r#"prose {"a": 1} tail"#), r#"{"a": 1} tail"#);

// Combo: prose + fence + damage in one call.
let reply = "Sure!\n```json\n{\"answer\": 42,}\n```\nDone.";
assert_eq!(repair_extract(reply).unwrap(), r#"{"answer": 42}"#);
```

## Parsing to a tree

`parse` gives a `Value` with accessor helpers, RFC 6901 pointers, and a
lossless `Number` that keeps the original digit text — 64-bit IDs and trailing
zeros survive a round trip byte for byte.

```rust
use jsonfix::parse;

let value = parse("{id: 1234567890123456789, price: 1.10}").unwrap();

// Numbers keep their exact text; parse them only if they fit.
let id = value.get("id").and_then(|v| v.as_number()).unwrap();
assert_eq!(id.as_str(), "1234567890123456789");
assert_eq!(id.as_u64(), Some(1234567890123456789)); // exact, never via f64
assert_eq!(
    value.get("price").and_then(|v| v.as_number()).unwrap().as_str(),
    "1.10"
);

// Trees: index / get / pointer (RFC 6901) / len.
let doc = parse(r#"{"users":[{"name":"ada"},{"name":"bob"}]}"#).unwrap();
// `[]` never panics: a missing key or index yields `null`, and values
// compare directly against strings, bools, and integers.
assert_eq!(doc["users"][1]["name"], "bob");
assert!(doc["users"][9]["name"].is_null());
assert_eq!(doc.pointer("/users/1/name").and_then(|v| v.as_str()), Some("bob"));
assert_eq!(
    doc.get("users").and_then(|u| u.index(0)).and_then(|u| u.get("name")).and_then(|v| v.as_str()),
    Some("ada")
);
assert_eq!(doc.get("users").map(|u| u.len()), Some(2));

// Render back to JSON text.
assert_eq!(value.to_json_string(), r#"{"id": 1234567890123456789, "price": 1.10}"#);
```

## Partial parsing and truncated input

While a model is still writing, the document ends mid-value. `Allow` says which
cut-off constructs may be kept; `parse_partial` (or `Options::partial`) enables
the repair passes *and* the policy.

| Flag | Keeps a cut-off… |
|---|---|
| `Allow::STR` | string (`"Hel`) |
| `Allow::NUM` | number (`1.`, `2e`, `-`) |
| `Allow::ARR` / `Allow::OBJ` | array / object |
| `Allow::KEY` | object key (`"x` → `"x": null`) |
| `Allow::BOOL` / `Allow::NULL` | `tru`, `fal` / `nul` |
| `Allow::ATOM` / `Allow::COLLECTION` / `Allow::ALL` | shorthands |

A member whose key is cut off is kept (as `null`) only with `Allow::KEY`;
otherwise the unfinished member is dropped:

```rust
use jsonfix::{parse_partial, Allow, Options};

let cut = r#"{"answer": "Hel"#;
let opts = Options::partial(Allow::OBJ | Allow::STR);
assert_eq!(parse_partial(cut, opts).unwrap().to_json_string(), r#"{"answer": "Hel"}"#);

let torn = r#"{"key": "v, "x"#;
// Unfinished member "x is dropped ...
assert_eq!(
    parse_partial(torn, Options::partial(Allow::OBJ | Allow::STR)).unwrap().to_json_string(),
    r#"{"key": "v"}"#
);
// ... unless Allow::KEY keeps it, as null.
assert_eq!(
    parse_partial(torn, Options::partial(Allow::ALL)).unwrap().to_json_string(),
    r#"{"key": "v", "x": null}"#
);
```

## Streaming token by token

`StreamRepairer` accepts the document chunk by chunk (LLM tokens) and, after
every chunk, returns exactly what repairing the whole input so far would
return. `push` re-renders the full repaired document; `push_delta` returns just
the change (`keep` this many bytes of your buffer, then append `text`) — which
is what a chat UI wants. `reset()` starts a new document.

```rust
use jsonfix::StreamRepairer;

let mut stream = StreamRepairer::new();
assert_eq!(stream.push(r#"{"name": "Ad"#).unwrap(), r#"{"name": "Ad"}"#);
assert_eq!(stream.push("a\", \"age\": ").unwrap(), r#"{"name": "Ada", "age": null}"#);
assert_eq!(stream.push("36}").unwrap(), r#"{"name": "Ada", "age": 36}"#);
assert_eq!(stream.value().unwrap().to_json_string(), r#"{"name": "Ada", "age": 36}"#);

// Deltas: patch an on-screen buffer in place.
let mut stream = StreamRepairer::new();
let mut shown = String::new();
for chunk in [r#"{"name": "Ad"#, "a\", \"age\": ", "36}"] {
    let delta = stream.push_delta(chunk).unwrap();
    shown.truncate(delta.keep);
    shown.push_str(delta.text);
}
assert_eq!(shown, r#"{"name": "Ada", "age": 36}"#);
```

## Choosing what gets repaired

Every repair pass is a bit in `Repairs` (`FENCES`, `COMMENTS`, `UNQUOTED`,
`KEYWORDS`, `NUMBERS`, `CONCATENATION`, `CALLS`, `ENTITIES`, `QUOTES`,
`WHITESPACE`, `TRUNCATION`, `NDJSON`; `ALL` / `NONE` shorthands), and
`Options::strict()` is a plain validator. With repairs off, `repair` reports
byte-precise errors instead of guessing.

```rust
use jsonfix::{repair_with, validate, Options, Repairs};

// Fences and comments only: a bare word stays an error.
let opts = Options::all().with_repairs(Repairs::FENCES | Repairs::COMMENTS);
assert_eq!(repair_with("```json\n{\"a\": 1} // done\n```", opts).unwrap(), r#"{"a": 1}"#);
assert!(repair_with("{a: 1}", opts).is_err());

assert!(validate(r#"{"a": 1}"#).is_ok());
assert!(validate("{a: 1}").is_err());
```

## Errors

Failures carry the class, a stable message, and a 0-based byte offset:

```rust
use jsonfix::{validate, ErrorKind};

let err = validate("[1, 2, 3,]").unwrap_err();
assert_eq!(err.kind(), &ErrorKind::TrailingComma);
assert_eq!(err.position(), 9);
assert_eq!(err.message(), "trailing comma");       // stable, no position
assert_eq!(err.to_string(), "trailing comma at byte 9");

// Wrapper errors display their inner error.
let mut sink = Vec::new();
let err = jsonfix::repair_to_writer("prose {\"a\": 1}", &mut sink, jsonfix::Options::all()).unwrap_err();
assert_eq!(
    err.to_string(),
    "repair failed: unexpected text around the first top-level value (try extract()) at byte 6"
);

let err = jsonfix::deserialize::<i32>("{a: 1}").unwrap_err();
assert_eq!(err.to_string(), "deserialization failed: invalid type: map, expected i32 at line 1 column 0");
```

## serde integration

`loads` returns a `serde_json::Value` in one call, `deserialize` reads straight
into your own types, and `from_value` does the same without `serde_json`.
Numbers outside the finite `f64` range survive as strings instead of erroring
the way `serde_json::from_str` does.

```rust
#[derive(serde::Deserialize, Debug, PartialEq)]
struct Reply { answer: String, score: f32 }

let reply: Reply = jsonfix::deserialize("{answer: 'yes', score: 0.9,}").unwrap();
assert_eq!(reply, Reply { answer: "yes".into(), score: 0.9 });

let value = jsonfix::loads("{n: 1e400}").unwrap();
assert_eq!(value["n"], "1e400"); // out-of-range text survives as a string

// `serde` only: no serde_json needed.
let reply: Reply = jsonfix::from_value(jsonfix::parse("{answer: 'yes', score: 0.9}").unwrap()).unwrap();
assert_eq!(reply, Reply { answer: "yes".into(), score: 0.9 });

// Value <-> serde_json::Value conversions (feature `serde_json`).
use jsonfix::Value;
assert_eq!(
    jsonfix::parse(r#"{"id": 1234567890123456789}"#).unwrap().to_serde_json()["id"],
    1234567890123456789u64
);
assert_eq!(Value::from_serde_json(&serde_json::json!({"a": 1})).to_json_string(), r#"{"a": 1}"#);
```

## Byte and writer APIs

For FFI/C-ABI sinks and `Vec<u8>` buffers: `repair_bytes` / `repair_bytes_with`
/ `repair_bytes_into` are alloc-only (no `std`) and byte-identical to `repair`.
For files and sockets: `repair_to_writer` (feature `std`) writes in one
`write_all` and never touches the sink on failure. `Value::write_to` appends
JSON text to any `String`.

```rust
// alloc only — no `std` needed.
assert_eq!(jsonfix::repair_bytes(b"{a: 1,}").unwrap(), br#"{"a": 1}"#);
let err = jsonfix::repair_bytes(b"{\"a\": \xff}").unwrap_err();
assert_eq!(err.position(), 6); // exact offset of the first bad byte

// Feature `std`: files, sockets, any io::Write.
let mut sink = Vec::new();
jsonfix::repair_to_writer("{a: 1,}", &mut sink, jsonfix::Options::all()).unwrap();
assert_eq!(sink, br#"{"a": 1}"#);

use jsonfix::parse;
let mut buf = String::from("prefix ");
parse("{a: 1}").unwrap().write_to(&mut buf);
assert_eq!(buf, r#"prefix {"a": 1}"#);
```

## Guarantees and edge cases

* **Lossless numbers**: `Number` never routes text through `f64`. `as_u64` /
  `as_i64` parse exactly (`None` if the text does not fit); `as_str` is always
  the original text; `as_f64` saturates (`1e400` → `inf`), so use `as_str`
  when exactness matters. Valid input round-trips unchanged (`1.10` stays
  `1.10`).
* **Depth cap**: nesting deeper than `MAX_NESTING_DEPTH` errors with
  `ErrorKind::DepthLimitExceeded` instead of overflowing the stack — even
  under full repair.
* **Prose is never invented, but bare words become strings**: `hello world`
  repairs to the JSON string `"hello world"` (the `UNQUOTED` pass doing its
  job); empty input errors with `NoValueFound`. Use `extract` first when only
  a JSON region should be considered.
* **Every pass is opt-out**, and `Options::strict()` accepts nothing.

## The rest of the surface

Tree accessors, streaming state, and bitmask algebra — the pieces the sections
above use but do not enumerate:

```rust
use jsonfix::{Allow, Repairs, StreamRepairer, Value};

// Every Value kind is matchable; objects keep insertion order and duplicates.
let kinds: Vec<&str> = jsonfix::parse(r#"[null, true, 1, "s", [], {}]"#)
    .unwrap().as_array().unwrap().iter().map(|v| match v {
        Value::Null => "Null", Value::Bool(_) => "Bool", Value::Number(_) => "Number",
        Value::String(_) => "String", Value::Array(_) => "Array", Value::Object(_) => "Object",
    }).collect();
assert_eq!(kinds, ["Null", "Bool", "Number", "String", "Array", "Object"]);
assert!(jsonfix::parse("null").unwrap().is_null());
assert!(jsonfix::parse("{}").unwrap().is_empty());
assert_eq!(jsonfix::parse(r#"{"a":1,"b":2}"#).unwrap().len(), 2);

// Number accessors are exact-or-None; f64 is the saturating convenience.
assert_eq!(jsonfix::parse("123456789012345678901234").unwrap().as_number().unwrap().as_i64(), None);
assert_eq!(jsonfix::parse("1.5").unwrap().as_f64(), Some(1.5));

// Build and edit trees: `From` conversions, `collect`, `get_mut`, `take`.
let mut reply = jsonfix::parse("{id: 7, tags: ['a']}").unwrap();
reply.get_mut("tags").and_then(Value::as_array_mut).unwrap().push(Value::from("b"));
*reply.pointer_mut("/id").unwrap() = Value::from(8);
assert_eq!(reply.to_json_string(), r#"{"id": 8, "tags": ["a", "b"]}"#);
let tags = reply.get_mut("tags").unwrap().take(); // moved out, `null` left behind
assert_eq!(tags, ["a", "b"].into_iter().collect::<Value>());

// Stream state: raw input, repaired output, byte count.
let mut stream = StreamRepairer::new();
stream.push(r#"{"a": 1"#).unwrap();
assert_eq!(stream.input(), r#"{"a": 1"#);
assert_eq!(stream.output(), r#"{"a": 1}"#);
assert_eq!(stream.len(), 7);
assert!(!stream.is_empty());

// Bitmask algebra: |, union, without, contains, bits, is_empty.
assert_eq!((Allow::STR | Allow::NUM).bits(), 3);
assert!(!Allow::ALL.without(Allow::KEY).contains(Allow::KEY));
assert_eq!(Repairs::FENCES.union(Repairs::COMMENTS).bits(), 3);
assert!(!Repairs::ALL.without(Repairs::NDJSON).contains(Repairs::NDJSON));
assert!(Allow::NOTHING.is_empty());
```

## Complete API reference

Every public item, in one place. `Options` fields (`allow`, `repairs`) are
public; `ErrorKind`, `WriteError`, and `DeserializeError` are
`#[non_exhaustive]`, so match them with a wildcard arm.

| Item | Returns | Notes |
|---|---|---|
| `parse(input)` / `parse_with(input, Options)` | `Result<Value, Error>` | tree from possibly-broken input |
| `parse_partial(input, Options)` | `Result<Value, Error>` | alias of `parse_with`, named for the partial case |
| `validate(input)` | `Result<Value, Error>` | strict JSON check |
| `repair(input)` / `repair_with(input, Options)` | `Result<String, Error>` | canonical JSON text |
| `repair_into(input, &mut String, Options)` | `Result<(), Error>` | append to a buffer; untouched on error |
| `repair_extract(input)` | `Result<String, Error>` | extract + repair, whole-input fallback |
| `extract(input)` | `Option<&str>` | first JSON span, unrepaired |
| `extract_all(input)` | `Vec<&str>` | every top-level span |
| `extract_partial(input)` | `&str` | first span through end of input |
| `repair_bytes` / `repair_bytes_with` | `Result<Vec<u8>, Error>` | byte output, alloc-only |
| `repair_bytes_into(input, &mut Vec<u8>, Options)` | `Result<(), Error>` | byte buffer, untouched on error |
| `repair_to_writer(input, &mut impl Write, Options)` | `Result<(), WriteError>` | *(feature `std`)* |
| `deserialize` / `deserialize_with` | `Result<T, DeserializeError>` | *(feature `serde_json`)* |
| `loads` / `loads_with` | `Result<serde_json::Value, DeserializeError>` | *(feature `serde_json`)* |
| `from_value(Value)` | `Result<T, serde::de::value::Error>` | *(feature `serde`)* |
| `MAX_NESTING_DEPTH` | `usize` constant | nesting cap (deeper input errors) |

| Type | Shape | Methods / fields |
|---|---|---|
| `Value` | `Null`, `Bool`, `Number`, `String`, `Array(Vec<Value>)`, `Object(Vec<(String, Value)>)` | `as_str`, `as_bool`, `as_number`, `as_f64`, `as_i64`, `as_u64`, `as_array`, `as_object`, `get`, `index`, `pointer`, `as_array_mut`, `as_object_mut`, `get_mut`, `pointer_mut`, `take`, `len`, `is_null`, `is_bool`, `is_number`, `is_string`, `is_array`, `is_object`, `is_empty`, `write_to`, `to_json_string`, `to_serde_json` / `from_serde_json` *(serde_json)*; `v["key"]` / `v[0]` (never panic, `null` when absent); `==` against `str`/`String`/`bool`/integers/floats; `From` bool, strings, integers, `()`, `Vec`s; `FromIterator` |
| `Number` | the original digit text | `as_str`, `as_f64`, `as_i64`, `as_u64` |
| `Options` | `allow: Allow`, `repairs: Repairs` | `all`, `strict`, `partial`, `with_repairs`, `with_allow`, `repairs`, `allows` |
| `Allow` | `NOTHING`/`NONE`, `STR`, `NUM`, `ARR`, `OBJ`, `KEY`, `BOOL`, `NULL`, `ATOM`, `COLLECTION`, `ALL` | `contains`, `is_empty`, `bits`, `union`, `without`, `BitOr` |
| `Repairs` | `NONE`, `FENCES`, `COMMENTS`, `UNQUOTED`, `KEYWORDS`, `NUMBERS`, `CONCATENATION`, `CALLS`, `ENTITIES`, `QUOTES`, `WHITESPACE`, `TRUNCATION`, `NDJSON`, `ALL` | `contains`, `bits`, `union`, `without`, `BitOr` |
| `Error` | kind + byte offset | `new`, `kind`, `position`, `message`; `Display`, `core::error::Error` |
| `ErrorKind` | `UnexpectedEnd`, `UnexpectedCharacter(char)`, `ExpectedObjectKey`, `ExpectedColon`, `NoValueFound`, `InvalidEscape`, `InvalidUnicodeEscape`, `ExpectedComma`, `TrailingComma`, `TrailingValue`, `UnquotedValue`, `DepthLimitExceeded`, `InvalidUtf8` | `#[non_exhaustive]` |
| `StreamRepairer` | accumulated input + rendered output | `new`, `with_options`, `push`, `push_delta`, `value`, `input`, `output`, `len`, `is_empty`, `reset` |
| `Delta<'a>` | `keep: usize`, `text: &'a str` | — |
| `WriteError` *(std)* | `Repair(Error)` \| `Write(io::Error)` | `#[non_exhaustive]` |
| `DeserializeError` *(serde_json)* | `Repair(Error)` \| `Json(String)` | `#[non_exhaustive]` |

## How it compares

Feature surface vs the other JSON-repair crates on crates.io (sources and
READMEs checked 2026-09; "—" means the crate does not ship the feature):

| | `jsonfix` | `jsonrepair` | `jsonrepair-rs` | `llm_json` | `repair_json` |
|---|---|---|---|---|---|
| Dependencies | 0 | 2 (`memchr`, `thiserror`) | 0 (opt. `serde*`) | 3 (`serde_json`, `clap`, ...) | 1 (`thiserror`) |
| `no_std` + `alloc` | ✔ | — | — | — | — |
| Lossless number text (u64 IDs, `1.10`) | ✔ | — | — | — | — |
| Repair passes you can toggle | 12 passes | 10 options | 1 (`strict`) | 4 bools | — |
| Partial-parse policy (per value kind) | 7 flags | — | — | 1 global toggle | streaming only |
| Incremental `Delta` output (`push_delta`) | ✔ | — | — | — | — |
| Byte-precise error positions | ✔ | — | — | — | — |
| Streaming API | ✔ | ✔ | ✔ | ✔ | ✔ |

The row that matters for LLM pipelines: when a stream truncates mid-number,
`jsonfix` keeps the digits it saw and reports byte-exact offsets — no other
crate in the table promises either.

## Performance

On the 13.2 KB fenced-LLM-reply fixture from the bench suite (`cargo bench -p
jsonfix-benchmarks --bench repair`):

| Operation | Time | Throughput |
|---|---|---|
| `repair` | ~276–283 μs | ~44–46 MiB/s |
| `repair_extract` | ~282–290 μs | ~43–45 MiB/s |

For context on the same machine, `validate` costs about the same as `repair`,
and both are ≈2× a native `serde_json::from_str` on the repaired output
(~190 μs). Numbers are single-machine criterion medians and move with the
machine; the ratios are the claim.

## No standard library required

`#![no_std]` with `alloc` only, and `#![forbid(unsafe_code)]` — which matters
when the input comes from an untrusted model or a hostile log line. The
optional `serde`/`serde_json` features are declared `default-features = false`
(alloc-only), so they do not pull `std` in.

## Development

```console
$ cargo test --all-features                   # full suite + doctests
$ cargo test --no-default-features --lib      # alloc-only surface
$ cargo clippy --all-targets --all-features -- -D warnings
$ cargo bench -p jsonfix-benchmarks --bench repair            # criterion
$ cargo +nightly fuzz run repair -- -runs=50000              # harness in fuzz/
```

Behaviour follows the widely used JavaScript `jsonrepair` for the cases it
defines, expressed here as a tolerant lexer plus a lenient parser; the
streaming and partial-parse policies go beyond it.

## License

Dual-licensed under MIT or Apache-2.0, at your option.