Build1 publisher2 min readPublished
Committed byte fixtures catch the ensure_ascii flip a dict-equality test lets through
A dev.to workflow freezes each json.dumps call site as committed UTF-8 bytes before anyone extracts a helper. The harness pins three dialects in one module and leaves one behaviour change unpinned.
The Engineer · Build desk

What happened
- A dev.to writeup sets out a workflow that freezes current json.dumps output as UTF-8 bytes, then permits one function extract and nothing else, with parsed dict equality explicitly barred from gating the change.
- Extracting to_json(data) without a byte pin merges those dialects into json.dumps defaults, and downstream tests keep passing because they decode the JSON and compare Python objects.
- Pins are captured once from the current call sites and committed as .json.bin binary fixtures, which any later extract has to reproduce byte for byte with no whitespace drift.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- exposure The consumers that break sit outside the repo doing the refactor: clients that parse key order, gateways that hash the raw body and reject reordered objects, and log pipelines that treat escaped Unicode as a new event class.
- decision ensure_ascii, sort_keys and separators become choices someone has to sign off on per site, because the writeup says reviewers rarely catch the ensure_ascii flip on its own.
- constraint What may go into a fixture is limited, and the limits matter: no pretty-print indent unless a site uses it, no dict equality after loads, no wall-clock timestamps, or the pins fail for reasons unrelated to the extract.
Take the harness's second fixture, the one named queue_compact_ascii. The payload is `{"title": "café", "ok": True}` and the recorded kwargs are `ensure_ascii=True` with `separators=(",", ":")` [8]. That call emits `{"title":"caf\u00e9","ok":true}`, 31 bytes, all of them ASCII [1]. Flip ensure_ascii to False and the same dict emits `{"title":"café","ok":true}`, 26 characters and 27 bytes, because é takes two [2]. The two bodies differ by four bytes and parse to the same dict [3]. Any check written as `loads(got) == expected` passes on both [16].
`assert_pins` copies each case's recorded kwargs, calls `dumps_fn(case.payload, **kwargs)`, encodes the result and raises `AssertionError` with "pin drift" when it does not match the committed fixture [10]. The function under test therefore has to accept json.dumps's keyword arguments. A `to_json(data)` helper taking one positional argument cannot be passed to it at all. The pin holds the three dialects still. I think that is the correct order of work: freeze the bytes, then change them on purpose, in a diff a reviewer can argue with.
The writeup asks each site to record three things: the exact UTF-8 bytes, the exception type dumps() raises on bad values, and whether `default=` was present [7]. `DumpCase` has three fields, and they are name, payload and dumps_kwargs [9]. That leaves the expected exception out, so the `TypeError("unpinned type: ...")` path in `_default` is not covered by any test in the file [11]. It is the merge case that needs it. A Decimal arriving at a site with no `default=` raises at the boundary today; on a helper that always passes `default=_default`, the same value serialises, since `str(Decimal("1.50"))` is "1.50" [12][4].
The author says the harness is a local example, not a measured production run [13]. For the fixtures to be worth copying into another repo, the kwargs at your call sites have to actually differ. The example module mixes sorted keys for cache stability, spaceless separators for a queue payload, and `ensure_ascii=True` for an old HTTP stack [4]. Where every dumps() in a module is already bare, a byte pin buys binary fixtures and no information. You also need captured payloads that resemble real traffic, because the instruction is to skip fields the call site never observed in production [18].
One test in the file, `test_write_pins_is_manual_only`, asserts that `write_pins` is callable, and the comment inside it says to run the capture from a shell rather than in CI [15].
Inventory comes before any of this. The step order in the writeup is `rg -n "json\.dumps\(|dumps\(" -g "*.py" app/`, record the kwargs, not the function name, note any wrapper that already calls dumps(), and do not extract during inventory [17].
What to watch
- Whether DumpCase grows a field for the expected exception type, since the prose asks each site to record it and the dataclass has no slot for it.
- Whether the author publishes a run against real call sites instead of the local example harness.
- A named client or gateway that rejected a reordered or re-escaped body would move the risk from asserted to documented.