What exactly should survive a round trip?
A text round trip sends data through a pipeline and reconstructs it at the other end. The phrase "lossless" must specify what is conserved: original bytes, Unicode scalar sequence, normalized equivalence class, or user-perceived rendering. These are distinct invariants. For example, UTF-8 and UTF-16 can represent the same scalar sequence with different bytes, while NFC can produce a canonically equivalent sequence that no longer contains the same code points as the input.
For a well-formed scalar string, encoding it as UTF-8 and decoding those bytes under strict UTF-8 should reconstruct the identical scalar sequence. The reverse also holds for well-formed UTF-8 bytes: decoding and then re-encoding produces the same bytes, assuming no BOM processing, newline conversion or normalization intervenes. Round-tripping across formats requires documenting which operations are permitted.
Model every transformation as a distinct boundary
A realistic text route might begin with a web form, cross the clipboard, enter JavaScript, be JSON-stringified, travel in HTTP, be parsed by a server, stored in a database, exported to CSV, and imported into a spreadsheet. Every boundary can change data independently: normalization, newline conversion, trimming, replacement decoding, tab expansion, numeric coercion or rich-text sanitization. A test that only compares the browser screen before and after cannot identify where a mismatch entered.
Capture a fixture before each boundary and record its hexadecimal scalars or encoded octets. Comparing only lengths can miss corruption: A and ฮ (Greek capital alpha) both contain one scalar but different identities, as do a plain Latin letter and an identically shaped mathematical styled letter. A transport may also preserve scalar identity but render it differently after font fallback.
Define four independent forms of equality
| Invariant | Suitable comparison | Typical application |
|---|---|---|
| Byte identity | Original bytes equal returned bytes | Cryptographic signature, binary file preservation |
| Scalar sequence | Exact ordered Unicode scalar values equal | Text editor, copied styled text, text storage |
| Canonical equivalence | Normalized NFC or NFD sequences equal | Canonical text search and deduplication |
| Rendered visual identity | Platform-specific shaped glyph appearance | UI screenshot regression (not encoding proof) |
Compatibility normalization is stronger and generally lossy: NFKC can map fullwidth A, mathematical bold A and ordinary A into the same string, eliminating distinctions. If a product promises to preserve decorative Unicode output, applying NFKC during export breaks exact scalar round-trip equality even if the recovered result remains legible.
Canonical equivalents are not identical stored strings
The character รฉ may be represented by U+00E9 or by U+0065 followed by U+0301. They are canonically equivalent, but the raw strings differ in code point count and UTF-8 bytes. NFC typically composes this particular sequence; NFD decomposes the precomposed character. Neither form is a universal reversible compressor of the original scalar stream.
const composed = "\u00E9"; // 1 scalar
const decomposed = "e\u0301"; // 2 scalars
console.log(composed === decomposed); // false
console.log(composed.normalize("NFC") === decomposed.normalize("NFC"));
// true
const hex = value => [...new TextEncoder().encode(value)]
.map(b => b.toString(16).padStart(2,"0")).join(" ");
console.log(hex(composed)); // c3 a9
console.log(hex(decomposed)); // 65 cc 81
Canonical equivalence is a text property. It does not imply identical bytes, equal JavaScript length, equal database key storage or equal grapheme segmentation in every tailored boundary system. Normalize intentionally at clearly documented points. If original form preservation matters, store the original alongside a normalized search or comparison key.
Correct UTF-8 / UTF-16 / UTF-32 conversion
UTF-8 bytes are variable-length sequences of one to four octets, while UTF-16 may use surrogate pairs and UTF-32 uses one 32-bit unit per scalar. A correctly implemented converter can preserve scalar identity across all three; it need not preserve original byte length or byte order. Always specify whether UTF-16/32 files are big-endian or little-endian and how BOMs are handled.
Invalid input requires a separate contract. A strict decoder rejects malformed octets or surrogate sequences. A replacement decoder inserts U+FFFD and cannot reconstruct the unknown original data. Testing decode(encode(s)) === s proves little if s was already damaged earlier; confirm the canonical fixture itself.
JavaScript round-trip and integrity fixture
JavaScript's TextEncoder converts scalar text to UTF-8 and TextDecoder converts bytes back. fatal: true rejects malformed UTF-8 instead of silently substituting replacement characters. Supplementary mathematical letters use two UTF-16 code units internally but one scalar, and a combining mark may remain separate even if the display looks like one letter.
const sample = "Aรฉ e\u0301 ๐ ๐";
if (!sample.isWellFormed()) throw new Error("Invalid UTF-16");
const bytes = new TextEncoder().encode(sample);
const roundTrip = new TextDecoder("utf-8",{fatal:true}).decode(bytes);
console.log(roundTrip === sample); // true
console.log([...roundTrip].map(ch =>
ch.codePointAt(0).toString(16)).join(" "));
// Exact scalar identities survive this controlled UTF-8 transport.
For binary integrity, hash or compare raw bytes before any text decoding and after an explicitly equivalent encoding cycle. For source-string identity, compare exact code points; do not rely on a normalization-based assertion unless canonical equivalence is actually the contract.
JSON and CSV are different kinds of containers
JSON serialization escapes selected characters but generally preserves the decoded scalar sequence of well-formed strings. A wire JSON payload exchanged outside a closed ecosystem should use UTF-8 under RFC 8259. Parsers may accept an isolated \uD800 escape, but its ill-formed surrogate value is not interchangeable as valid Unicode scalar text, so validate parsed strings before passing them to UTF-8 encoders.
CSV lacks a single universal encoding or dialect rule across every importer. Spreadsheet programs may guess a legacy charset, interpret delimiters or line endings differently, change leading zeros, or apply type inference to strings. For CSV, lock the encoding to UTF-8, specify separator and quoting rules, validate schema, and disable unwanted numeric or date coercion where the importer permits. A BOM may assist some CSV consumers but is not universally necessary or desirable.
Database collation versus stored value integrity
Database collations can compare strings as equal under accent-insensitive or case-insensitive rules even when their bytes and scalars differ. A unique index with a particular collation might therefore reject one of two distinct Unicode strings without altering the stored bytes. Use an explicitly documented collation and normalization policy for identifiers. Database column encodings, driver settings and length semantics must also be validated.
A field declared for "50 characters" may enforce bytes, UTF-16 code units, scalars or another database-specific length metric. Since mathematical styled characters occupy four UTF-8 bytes but may count as one scalar, valid user-visible output can exceed a byte-based limit unexpectedly. Treat size limits as transport and schema requirements, not as evidence of corruption.
Clipboard, HTML and styled text
Copy operations often offer text/plain and text/html simultaneously. A receiving program may choose a rich representation rather than the plain-text one. If text appearance came from CSS bold, plain-text paste usually preserves ordinary letters without CSS. If a string contains actual U+1D400 mathematical bold characters, plain-text paste can preserve those characters even if the destination uses a different font.
That distinction matters when evaluating the US Fancy Text tool: compare the exact styled Unicode code points before and after copying, not only visual similarity. If a platform replaces unsupported glyphs, use a code-point inspector to tell whether data changed or only the font rendering changed.
Strict Python UTF-8 and JSON verification
import json
import unicodedata
original = "cafรฉ e\u0301 ๐ ๐"
raw = original.encode("utf-8", errors="strict")
assert raw.decode("utf-8", errors="strict") == original
wire = json.dumps({"text": original}, ensure_ascii=False).encode("utf-8")
restored = json.loads(wire.decode("utf-8"))["text"]
assert restored == original
assert unicodedata.normalize("NFC", "e\u0301") == "\u00E9"
assert "๐".encode("utf-8").hex() == "f09d9080"
When testing a production pipeline, repeat with both precomposed and decomposed forms. Include samples containing NUL where the protocol allows it, non-Latin text, combining sequences and emoji ZWJ sequences. A protocol that forbids some scalars must report its rejection distinctly from a decoder rejecting malformed UTF.
An interoperability matrix that identifies the failing layer
| Fixture | Expected invariant | Potential failure stage |
|---|---|---|
ASCII A | Byte and scalar identity in UTF-8 round trip | Wrong delimiter or field decoding |
| U+00E9 versus U+0065 U+0301 | Exact difference, canonical equivalence | Unwanted normalization |
| U+1D400 mathematical A | Exact scalar identity | NFKC folding or legacy charset |
| U+1F600 emoji | One scalar, UTF-16 pair | Unsafe code-unit slicing |
Invalid UTF-8 C0 AF | Strict decoder rejects | Decoder disagreement |
| Copied text with rich markup | Document chosen clipboard representation | HTML versus plain-text paste |
CSV identifier 00123 | Preserve leading zeros as text | Spreadsheet type inference |
Use the same fixture set on browser, server, database, export and import paths. Log the precise before/after bytes or scalar sequence at each boundary. When a mismatch is found, isolate the first transformation responsible instead of applying a global Unicode "cleanup" that risks obscuring the original failure.
Standards and technical references
- UAX #15 โ Unicode Normalization Forms
- RFC 8259 โ JSON interchange and UTF-8
- RFC 4180 โ Common CSV format conventions
- Unicode FAQ โ UTF encoding forms and BOMs
- MDN โ TextEncoder
- MDN โ Clipboard API
Independent technical research. Byte representation, scalar identity and presentation are separate interoperability dimensions.