Letras Diferentes
Unicode research · Cross-system normalization contracts

Unicode Normalization and Data Consistency Across APIs, Databases and Indexes

Engineering design for normalization contracts, versioned NFC storage and search keys, safe migrations, streaming, security boundaries and cross-service data integrity.

Technical referenceUnicode baseline: 18.0.0

A system can be internally inconsistent without containing invalid UTF

Suppose a browser submits a username containing U+00E9 while a backend stores the canonically equivalent two-scalar sequence U+0065 U+0301. Both represent well-formed Unicode, but binary comparisons, hash digests, length calculations and unique constraints may disagree. Such a mismatch is a normalization policy problem rather than malformed UTF-8 or mojibake. It becomes difficult to diagnose when some services normalize on write, others normalize only on query, and another silently applies NFKC.

A robust architecture specifies which representations are authoritative. Unicode normalization is part of the data contract: where it is applied, which form is used, whether source scalars must be preserved, which UCD version backs it, and what happens to ill-formed input. Stating simply “we use Unicode” does not answer these questions.

Four normalization forms have different preservation properties

FormAlgorithmLikely useRisk
NFCCanonical decomposition plus compositionStored normalized text or exact canonical comparisonOriginal scalar spelling is lost
NFDCanonical decomposition plus mark orderingText analysis and canonical keysMore scalars than source for some characters
NFKCCompatibility decomposition plus compositionExplicitly specified identifier/search profilesMathematical style, width, enclosure can disappear
NFKDCompatibility decomposition, no recompositionSome specialized analysis pipelinesLossy identity and expansion

No form guarantees that a string can be restored to the exact original scalar sequence. In particular, NFKC changes the identity of many styled mathematical or circled characters. If a product promises faithful copy and paste, it should not overwrite the sole stored copy with compatibility-folded output.

Store raw display text and a derived comparison key separately

One practical schema uses display_text for the user's original well-formed string, a derived canonical_key for exact canonical equivalence (for example NFC), and optional purpose-specific search_key fields for case folding, accent-insensitive search or collation. These keys are deliberately not interchangeable: a binary equality key, a fuzzy search key and a natural-language sort key need different guarantees.

For identifiers, define a profile with allowed scripts and character sets rather than assuming NFKC makes names secure. Case folding and confusables detection are additional operations, each with its own Unicode data dependencies. Keep these transformations out of rich-text editing and mathematical notation data unless there is explicit product justification.

Normalize at agreed boundaries, not opportunistically

A frontend may normalize text before sending JSON, while an API gateway validates only byte well-formedness and a database connector normalizes again. Reapplying the same Unicode normal form is idempotent, but introducing a different form, case mapping or accent stripping at another boundary changes the contract. Designate an authoritative normalization boundary, document client expectations and validate incoming data against the same rule.

Browser strings are UTF-16 code-unit sequences and can contain unpaired surrogates in some circumstances; this is not well-formed Unicode scalar data for interchange. Explicitly reject or replace ill-formed input according to protocol policy before computing identifiers. Silent replacement can make two previously different invalid inputs collide.

JavaScript: derive a canonical key without losing source

function recordText(original) {
  if (!original.isWellFormed()) {
    throw new TypeError("Ill-formed UTF-16 input");
  }
  return {
    displayText: original,
    canonicalKey: original.normalize("NFC"),
    normalizationForm: "NFC"
  };
}
const a = recordText("e\u0301");
const b = recordText("\u00E9");
console.log(a.displayText === b.displayText); // false
console.log(a.canonicalKey === b.canonicalKey); // true
console.log(recordText("𝐀").displayText.normalize("NFKC")); // A

The example is a data-model illustration rather than a complete security or database schema. It preserves the user's input for display and derives a comparison value. If an application needs to sign the request, specify whether it signs transmitted UTF-8 bytes, source text or the canonical key; each can produce a different digest.

Normalization is not safely chunk-local

Even if two separate chunks are each normalized to NFC, their concatenation may not be NFC. The first chunk could end in e and the second begin with U+0301 COMBINING ACUTE ACCENT; together they compose to U+00E9. Canonical ordering can also require reordering marks across a chunk boundary. Normalizing arbitrary file chunks independently then concatenating their outputs does not implement whole-stream normalization correctly.

const left = "e".normalize("NFC");
const right = "\u0301".normalize("NFC");
const together = left + right;
console.log(together === together.normalize("NFC")); // false
console.log(together.normalize("NFC") === "\u00E9"); // true

UAX #15 defines ways to identify safe boundaries and addresses stream-safe text processing. A streaming normalizer should use a tested implementation with appropriate boundary state; it should not just store the last UTF-16 code unit and assume that is enough context for every combining sequence.

Migrating existing unnormalized records

Before changing the normalization policy on a live dataset, run a read-only audit to identify records whose derived keys collide. For instance, a user table may contain both e + acute and é as separate byte strings. After NFC normalization they become the same key; blindly creating a unique constraint can fail or overwrite data. Design collision-resolution rules that respect ownership, product behavior and auditability.

A safe migration generally computes derived candidate keys, reports collision groups, resolves them under explicit business rules, adds or rebuilds indexes, switches readers/writers to the same policy and preserves enough information for rollback. Cryptographic hashes and signatures based on raw bytes must be recalculated only when the protocol authorizes canonicalization; never silently rewrite signed payloads.

Unicode versions, collation versions and consistency windows

Unicode has strong normalization stability policies for established characters, but implementations may bundle different UCD versions. ICU and CLDR collation data have their own update cadence and can change sorting or equivalence decisions. Record the algorithm name, locale where applicable, and data/library version for derived search and sort keys, especially if serialized to persistent storage or shared across services.

If a website has a browser, Node.js runtime, Python worker and database, test a common fixture set through all four paths. Do not assume that Intl.Collator, database accent-insensitive collation, Python casefold() and NFC define the same equivalence relation. Build keys in one authoritative layer or enforce explicit cross-service conformance tests.

Python validation and non-destructive storage

import unicodedata as ud
def prepare_record(value):
    if any(0xD800 <= ord(ch) <= 0xDFFF for ch in value):
        raise ValueError("surrogate scalar is not valid interchange text")
    return {"original": value, "nfc": ud.normalize("NFC", value)}
a, b = prepare_record("e\u0301"), prepare_record("\u00E9")
assert a["original"] != b["original"]
assert a["nfc"] == b["nfc"]
assert prepare_record("𝐀")["original"] == "𝐀"
print("UCD:", ud.unidata_version)

Comparison profiles should be consistent end-to-end

A system that normalizes a username only after checking the allowlist can introduce new characters or equivalences that the validator did not anticipate, especially with compatibility normalization. Normalize and validate in a documented order; ensure registration, authentication, lookup and uniqueness constraints use the same canonical identity key. UTS #39 supplies confusable and identifier guidance, but normalized text by itself is not proof of safe content.

Search keys can intentionally match more than authentication keys. This is appropriate if they remain separate. A user searching for “cafe” may want results containing “café,” but no one should assume those two distinct strings must be treated as the same account identifier.

A contract-based regression plan

FixtureInvariant to assert
é versus e + U+0301Same NFC key; originals remain distinct
U+1D400 versus U+0041NFC keys remain distinct; NFKC would collapse them
Hangul syllable vs conjoining JamoCanonical keys agree
NFC chunks joined across accentWhole-string normalization is still required
Existing database colliding keysDetect and resolve before adding uniqueness constraint
Invalid surrogate inputReject or explicitly replace before UTF-8 interchange
ICU or Unicode version upgradeRe-run fixtures and rebuild version-dependent indexes

A website offering copyable text should treat preservation as a first-class invariant. The US Fancy Text tool can furnish supplementary mathematical letters, combining marks and other styled characters for round-trip tests. Its original text must remain distinct from any lossy index derived to improve matching.

Standards and implementation references

  1. UAX #15 — Unicode normalization and streaming
  2. UAX #44 — UCD versioned properties
  3. Unicode FAQ — normalization contracts
  4. UTS #39 — Identifier and security policies
  5. MDN — String.isWellFormed
  6. Python unicodedata

Independent technical reference. Preservation, comparison, segmentation and presentation use different Unicode rules.