Hardware FixRecommendedDevice not working? Your driver may be the problemCheck updates for common hardware issues.Fix DriversOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsPC HealthRecommendedCrashes, freezes, slowdowns? Check your PC nowSpot repairable issues before they interrupt work.Check PC×
Skip to content
World desk6 min

Pin JSON Bytes and Default Handlers Before One Serializer Extract

Two json.dumps() call sites can decode to equal Python objects and still emit different bytes. Here is how to pin the exact output and error behavior before you consolidate them.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Before you merge several json.dumps() call sites into one helper, record the exact bytes each site emits today and the error each one raises on bad input. Two call sites can load back into equal Python objects and still send different text over the wire. A byte-level pin catches that difference; a comparison of parsed dictionaries does not.

Why parsed equality is not enough

When a Python module is refactored, tests usually call json.loads() on the output and compare dictionaries. That check answers one question: do the decoded values match? It does not answer whether the text changed. Key order, whitespace, the escaping of non-ASCII characters, and the way NaN is written can all change while the decoded object stays identical.

As an Amazon Associate I earn from qualifying purchases.

The problem appears most often in older modules where each function passes its own keyword arguments to json.dumps(). One function may sort keys and use compact separators, another may keep the defaults, and a third may escape every non-ASCII character. A single helper with default settings would quietly merge those dialects. Any client that hashes, signs, diffs, or stores the raw body would then see a change even though no test failed.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The approach described here comes from Dakota Huang’s DEV Community post dated “Sep 16” (the page shows no year). The steps are that author’s recommendations, not independently tested findings, and they are most useful when your code sends or stores serialized bytes.

Step 1: Inventory call sites and their keyword arguments

Before you change any code, list every place that calls json.dumps() and record the keyword arguments each one passes. A search from the repository root is a practical start:

rg -n "json.dumps(" src/

For each hit, write down the kwargs, the payload type, and whether a default= handler is present. Group only sites whose settings are identical. Sites that differ in even one setting belong in separate groups, because they are separate dialects.

Step 2: Pin exact UTF-8 bytes and error behavior

For each dialect, choose a small representative payload and capture the output as bytes, not as a parsed object. Encode the string with UTF-8 and store it as a binary fixture. The pin should also record the exception type for any value that fails to serialize, plus whether a default= handler was in effect. A TypeError and a ValueError are different behaviors, and a refactor that changes one into the other is a real break.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The author’s sample harness uses a dataclass to describe each case, keeps the fixtures in a directory of binary files, and compares freshly generated bytes against them. The following sketch shows the shape. It is an illustration of the method, not a measured production run.

import json
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal

@dataclass(frozen=True)
class Case:
    name: str
    payload: object
    kwargs: dict

def handler(obj):
    if isinstance(obj, datetime):
        return obj.isoformat()
    if isinstance(obj, Decimal):
        return str(obj)
    raise TypeError(f"not serializable: {type(obj).__name__}")

def emit(case):
    return json.dumps(case.payload, **case.kwargs).encode("utf-8")

CASES = [
    Case("sorted_compact", {"b": 1, "a": "x"},
         {"sort_keys": True, "separators": (",", ":")}),
    Case("compact_non_ascii", {"name": "José"},
         {"separators": (",", ":")}),
    Case("spaced_decimal_datetime",
         {"amount": Decimal("12.50"), "at": datetime.fromisoformat("2026-10-08T09:00:00+00:00")},
         {"default": handler}),
]

Each case writes its bytes to tests/fixtures/json/<name>.bin. Inspect a fixture with a hex dump when you need to see exactly what is there:

xxd -l 64 tests/fixtures/json/compact_non_ascii.bin

Pin only what each call site actually uses. If a site never passes allow_nan, do not add it to the pin, because that adds a setting nobody depends on.

The keyword arguments that change output

The author lists six settings that can alter either the emitted text or the error raised. The table summarizes what each one changes and what to record when you pin it.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Setting What it can change What to record in the pin
sort_keys Key order in the output. Dictionaries with the same items in a different insertion order produce different bytes when it is True. Whether it is set, and a payload whose keys are inserted out of order
ensure_ascii Whether non-ASCII characters are written as uXXXX escapes (the default) or as UTF-8 characters. A payload containing at least one non-ASCII character
separators The whitespace after commas and colons. The default is (", ", ": "); compact output uses (",", ":"). The exact tuple passed
default The handler called for objects the encoder does not support. It determines whether a value serializes or raises. Whether a handler exists, its identity, and the exception it raises for unknown types
allow_nan With the default True, NaN and infinity values are written out. With False, they raise ValueError. The setting and the exception type for a non-finite float
skipkeys With the default False, a key that is not a basic type raises TypeError. With True, that key is dropped silently. The setting and a payload with a non-basic key

Three of these settings are the ones most often overlooked when code is consolidated. sort_keys and separators change bytes on valid input. default and skipkeys change which inputs fail, and how. allow_nan decides whether a non-finite float is a silent output or a loud error.

Step 3: Extract one kwargs group at a time

The author’s suggested sequence keeps each change small enough to verify. Follow it in this order:

  1. Commit the inventory and the binary fixtures on their own, before any extraction.
  2. Prove the pin can fail. Deliberately change one option in a throwaway branch, such as swapping sort_keys, and confirm the byte comparison reports a difference.
  3. Extract a helper for exactly one kwargs set. Keep the helper’s defaults identical to that group’s settings, not to the library’s defaults.
  4. Update only the call sites that belong to that group. Leave every other site alone.
  5. Inspect the diff. Every changed line should be a call-site swap, with no edits to settings or payloads.
  6. Rerun the pin suite. The fixtures must still match byte for byte.

The most important rule is at step 6. If a pin fails after extraction, the helper is wrong or the call site was moved to the wrong group. Do not regenerate the fixtures to make the failure disappear. A regenerated fixture records whatever the new code emits, which means it no longer tests anything.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Where byte pins fall short

A byte pin tests representation. It does not test whether the JSON describes the right data. The author states that byte pins do not establish schema correctness, and that drift in field names, types, or required keys needs a separate contract test. A pin and a contract test check different things, and one cannot replace the other.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The method also needs care in three situations:

  • Streaming JSON lines with timestamps. Each line changes on every run unless the clock is controlled. Freeze the clock for any payload with a time field, or the pin will fail for reasons that have nothing to do with the refactor.
  • Unordered set iteration. If a default handler converts a set by iterating over it, the output order can vary between runs. Sort the elements in the handler before serializing, so the bytes are deterministic.
  • Standing in for an HTTP contract test. A fixture of a request body shows what your code wrote, not what the server accepts. Keep the HTTP-level test for that.

Also treat an intentional pretty-print change, such as adding indent=2, as a new dialect. Give it its own case and its own fixture rather than editing the existing pins.

When to skip the extraction

The author advises against this extraction in several conditions, and they are worth checking before you start:

  • There is no byte-level test runner yet. Build the runner first.
  • All call sites already share one kwargs dictionary, so there is nothing to merge.
  • The module only emits debug logs, where the exact bytes are not observed by any client.
  • Project policy forbids committing payload shapes to the repository.

The author also claims that output for the flags discussed is stable on current CPython. That is the author’s assertion, not a compatibility guarantee from the Python project. Rerun the pins whenever the interpreter changes, and run them on a second runtime if your deployment uses more than one.

As the author puts it, “Wire clients consume bytes, not Python dicts.” That sentence is the reason to pin bytes before you move any call site.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The DEV Community post is the only source behind this article, so its recommendations reflect one author’s workflow. Check the behavior of each keyword argument against the Python standard library documentation for your version before you rely on it.

“

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.