jsonrescue: every whole value out of JSON no parser will open¶
The problem¶
Much of the "JSON" on disk is not one document: objects written back to
back with no array around them, a JSON Lines log whose writer died
mid-line, a download cut off half way through a big array, a file that
opens half way through an object. json.load refuses it all at the first
stray byte. jq reads back-to-back values but stops at the first broken
one: on a real 505-value stream with three values torn, it returned 10.
The method¶
- Decode one value, note where it stopped, go on.
json.JSONDecoder.raw_decode(text, i)parses a single value and returns where it ended. That reads{..}{..}, JSON Lines, RFC 7464 record separators and comma-joined values alike. - After a tear, skip only to a real top-level start. In valid JSON a
nested
{or[always follows:,,or[. A bracket after anything else (cut-off text, a closing bracket) starts the next value. A bracket opening a line counts only at the column top-level values sit at, so the members of a pretty-printed value are never lifted to the top. - Enter a cut-off array, one level, and keep its whole elements; a half element is dropped. A whole array stays one value.
- Report every tear by line and column, and the share parsed. Complete
objects inside torn spans go to a separate file with
--fragments.
def starts_value(text, p):
"""After a tear: does the bracket at p open a top-level value?"""
q = p - 1
while q >= 0 and text[q] in " \t\r":
q -= 1
return q < 0 or text[q] not in ":,[" # after these it is nested
jsonrescue export.json > values.jsonl # one value per line
jsonrescue --array cut.json > fixed.json # one valid array
jsonrescue --fragments inner.jsonl torn.json # keep pieces of torn values
Exit 0 when every byte was accounted for, 1 when something was torn, 2 when the input could not be read.
Limits¶
- A cut right after
:,,or[makes the next value look nested; it is skipped with the torn one (--fragmentskeeps it). - A tear inside a minified array loses the rest of that array.
- Only objects, arrays and record separators are resync points.
NaN/Infinitybecomenull, invalid UTF-8 becomes U+FFFD; both counted.- The whole input is read into memory.
Code¶
methods/jsonrescue/ in the methods repository: one Python standard-library file and a 39-check test, including 40,000-value streams with tears that must come back whole and in linear time.