Reading — step 1 of 5
Read
Putting It All Together
You have every stage of a real JSON library:
bytes --tokenize()--> tokens --parseValue()--> value --serialize()--> bytes
Composing serialize after parse gives round-tripping: the output means the same thing as the input. Note "means the same" — not "is byte-identical". Whitespace vanishes, key order may change, 1.5e2 becomes 150.0. When you need byte-identical output for the same value — hashing, signing, deduplication — you impose extra rules and call the result a canonical form (RFC 8785, JCS, is the standardized version; JWS signing depends on it). That is this exercise: compact separators, sorted keys, fixed number formatting.
The number-formatting contract
Canonical output must be deterministic, so pin the rule — and here it is the one your parser already implies:
- A literal without
./e/Eparsed to an int: emitstr(v)—42→42. - A literal with them parsed to a float: emit
repr(v)—1.5e2→150.0,1.50→1.5(Python's repr is the shortest text that round-trips the double).
The subtle consequence: whole-valued floats keep their .0. The int/float distinction made at parse time is information, and canonical output preserves it rather than collapsing 150.0 to 150.
What we deliberately skipped
Where your parser sits in the wider ecosystem — worth knowing, easy to add later:
- JSON5 / JSONC — relaxed supersets (comments, trailing commas, unquoted keys) used for config files people edit (VS Code settings, tsconfig.json). Not interchange formats; strict parsers reject them.
- JSON Lines (
.jsonl) — one document per line; logs and ML datasets. Your line-based harness has been secretly doing this all course. - JSON Schema — declarative validation of parsed documents (types, required keys, ranges).
- simdjson — SIMD instructions to parse at ~2–3 GB/s; same output as yours, different engineering planet.
- Binary cousins — MessagePack, CBOR, Protocol Buffers: smaller and faster, at the price of the universal, debuggable text form that made JSON win.
After building lexer, parser, positions, depth guard, and serializer, you know what json.loads actually does — and why "just use JSON" remains the right first answer for most interchange problems.
Your exercise
Canonicalize each line: parse, then re-emit compact (, and : with no spaces) with object keys sorted — { "b": 2, "a": 1 } prints {"a":1,"b":2}. The grader-caught mistake is number collapsing, and the starter itself commits it: its canonical() contains an is_integer() branch that turns the float 150.0 into 150 — but the hidden test feeds 1.5e2 and expects exactly 150.0. Delete that branch: ints via str, floats via repr, never convert between them. (The description's 3.0 → 3 example describes the buggy branch — trust the tests.) Also: "\u00e9" must print "é" — emit the real character, not the escape (ensure_ascii=False if you quote strings with the stdlib), and scalars are valid documents (true → true, "hello" → "hello").
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…