Desk Clarity

Everyday document & browser help

JSON Shows Unicode Escapes: Separate Representation, UTF-8 Bytes and Double Encoding

By ON Lab · Sources checked November 8, 2026

A JSON file showing \uXXXX sequences is not necessarily damaged. Those escapes can preserve the original Unicode characters exactly. In Python 3, ensure_ascii=False changes the JSON text representation; the file’s encoding is a separate choice. Verify by loading the saved JSON and comparing the actual value.

Compare escaped and literal representations

import json
data = {"label": "한글 café"}
escaped = json.dumps(data)
literal = json.dumps(data, ensure_ascii=False)
print(escaped)
print(literal)
assert json.loads(escaped) == data
assert json.loads(literal) == data
print(type(literal).__name__)

Our Python 3.13.18 fixture round-tripped both forms to the same object. The second result was a Python str, not an encoded byte buffer. The ASCII-escaped form is valid JSON too; a display preference is not evidence of character loss.

The historical public questions include Python 2 examples and its different str/unicode types. This guide uses current Python 3 behavior and does not transfer old encoding-keyword or mixed-type workarounds into the examples.

Choose the file encoding explicitly

from pathlib import Path
import json
import tempfile
data = {"label": "한글 café"}
with tempfile.TemporaryDirectory() as temporary:
    path = Path(temporary) / "data.json"
    with path.open("w", encoding="utf-8") as stream:
        json.dump(data, stream, ensure_ascii=False)
    with path.open(encoding="utf-8") as stream:
        restored = json.load(stream)
    assert restored == data
    print(path.read_bytes().hex())

This wrote and reopened a temporary UTF-8 file successfully. The JSON encoder supplies text to the stream; the stream turns it into bytes according to its encoding. Choosing ensure_ascii=False does not independently select a file encoding, a terminal font or the consumer’s decoding policy.

If a byte-level audit is needed, inspect the bytes as well as the loaded value. A readable terminal display is not proof that a saved file uses the intended encoding, and an escaped display is not proof that the underlying characters are lost.

Literal backslash text must remain escaped

import json
data = {"literal": r"\uD55C"}
wire = json.dumps(data, ensure_ascii=False)
print(wire)
assert json.loads(wire) == data

Here the input intentionally contains a backslash and the letters/digits uD55C. It is not the Korean character represented by that escape in a JSON string. Our encoder preserved the literal text, including the necessary JSON escaping of the backslash. Turning off ASCII escaping does not mean turning off JSON syntax rules.

Do not apply a blanket unicode-escape decode to already decoded application data. That can reinterpret intentional backslashes or control sequences. Use the JSON parser for actual JSON text, and apply any further domain-specific escape convention only when the producer’s contract requires it.

Check for a second serialization layer

import json
data = {"label": "한글 café"}
once = json.dumps(data, ensure_ascii=False)
twice = json.dumps(once, ensure_ascii=False)
restored_once = json.loads(twice)
print(type(restored_once).__name__)
assert isinstance(restored_once, str)
assert json.loads(restored_once) == data

After one load of the twice-serialized value, our result was a string, not the original dictionary. The extra escaping was a consequence of serializing JSON text as another JSON string. Repeatedly decoding until output looks right is not a reliable general repair; decide whether the interface expects an object or a string containing JSON.

Use a complete acceptance check

  1. Identify whether the input is a Python object, JSON text or encoded bytes.
  2. Choose the intended readable representation without treating valid escapes as corruption.
  3. Write and read using the agreed file encoding.
  4. Compare both the loaded type and value with the intended object.
  5. If they differ, inspect the actual producer/consumer boundary before adding another encode/decode layer.

The CSV BOM guide addresses a different file-decoding boundary. A JSON escape is not a UTF-8 BOM, and changing a CSV codec is not a fix for double JSON serialization.

The standard library and original public answers are free alternatives. No paid Unicode converter is necessary for these controlled checks. The examples do not certify every unpaired-surrogate, legacy-codec, terminal or external-API behavior.

Method: Python 3.13 JSON documentation, historical public questions and synthetic representation/file/backslash/double-layer cases on Python 3.13.18/macOS. No actual user file, Python 2 runtime, external API or every Unicode edge case was tested.

Sources