Skip to content

Unicode identifier review guide

Unicode normalization and identifier comparison

Compare identifier strings deliberately when equivalent-looking Unicode sequences can have different code point sequences. Preserve source text, choose a normalization policy, detect collisions, and document the Unicode version used by the review.

Do not compare Unicode identifiers by visual appearance alone. Keep the source string, choose a stated normalization form for the identifier role, apply the same policy to both sides of a comparison, and retain the original when reporting a match or collision. NFC is a common choice when canonical equivalence should compare together. NFKC can erase compatibility distinctions, so use it only when that loss is intentional and documented. This is an identifier comparison method, not proof that two names refer to the same real-world entity.

Updated

On this page

Separate visual equality from string equality

Two identifiers can render the same way while carrying different code point sequences. A composed character and a base character followed by a combining mark are the standard synthetic test. A reviewer who prints only the value may see no difference; a byte comparison, hash, or exact join can still distinguish them. Start by preserving the source text and inspecting code points or a safe escaped representation.

The source dates shown in this guide identify documentation checks on 22 October 2026, not original publication dates. Record the Unicode version, runtime, and normalization library used by the review. A future Unicode or runtime update may change which characters are permitted or how an application treats them, so the review should be reproducible rather than implied by a display screenshot.

Separate visual equality from string equality (table)
Comparison resultWhat it tells youWhat it does not tell you
Visual matchThe rendered text looks alikeThe strings are byte or code point equal
Exact matchThe source sequences are equalThe values are canonically equivalent
NFC matchThe chosen canonical forms are equalThe identifiers have the same business owner
NFKC matchCompatibility distinctions were treated as equalThat loss is acceptable for every consumer
CollisionDifferent source strings share a comparison keyWhich record should win

Choose a normalization policy per field

Do not normalize every string in a dataset with one blind replacement. A display label, an exact external key, a search key, and a protocol identifier have different contracts. For each field, decide whether the original text is authoritative, whether a comparison key is derived, whether case folding is allowed, and what happens when two originals produce the same key.

Choose a normalization policy per field (table)
Policy fieldWorksheet choiceDecision to document
originalpreserveWhich value is shown and exported?
normal_formNFC, NFD, NFKC, NFKD, or noneWhich equivalences are intentional?
case_policyexact, lower, or a specified foldIs case part of identity?
comparison_keyderived_and_not_authoritativeCan the key be regenerated?
collision_actionhold_for_reviewCan distinct originals map together?
unicode_versionrecord_runtime_versionCan another reviewer reproduce it?

UAX #15 explains that compatibility normalization can remove distinctions that may matter to identifiers. Use NFKC only when the field owner has decided that those distinctions are not identity-bearing. UAX #31 offers identifier guidance, but a standard default still needs to be reconciled with the producer contract and the receiving system Unicode Consortium, (opens in a new tab) Unicode Consortium, (opens in a new tab).

Derive a comparison key and retain the original

A comparison key is useful only when it is a declared derivative. Store it beside the original, include the chosen form and version in the review record, and never overwrite the source text just to make a join pass. The key can support an exact, canonical, or compatibility comparison, but it cannot decide a business collision by itself.

Derive a comparison key and retain the original code example, Python
import unicodedata

left = "cafe\u0301"
right = "caf\u00e9"

def comparison_key(value: str) -> str:
    return unicodedata.normalize("NFC", value)

assert left != right
assert comparison_key(left) == comparison_key(right)
review = {
    "original_left": left,
    "original_right": right,
    "comparison_left": comparison_key(left),
    "comparison_right": comparison_key(right),
    "form": "NFC",
    "case_policy": "exact",
    "unicode_version": unicodedata.unidata_version,
    "key_equal": True,
    "action": "compare_owner_and_context",
}

Collision worksheet

  1. Capture both original strings in an escaped or code point-safe form.
  2. Record the form, case rule, Unicode version, and comparison key.
  3. Check whether the producer says the originals are the same identifier.
  4. Hold the pair when different originals collapse to one key without an owner rule.
  5. Publish the comparison result without erasing the source evidence.

Test exports, joins, and canonicalization separately

Run the policy at every boundary where strings are parsed, stored, indexed, sorted, hashed, or exported. A source may preserve an original while a warehouse key normalizes it, or a JSON serializer may preserve a string while a downstream comparison silently applies a different case rule. Test the same fixture through each system and compare both the original and derived key.

Test exports, joins, and canonicalization separately (table)
BoundaryTestEvidence to retain
Input parseInspect code points before normalizationEscaped original and parser version
StorageRead back original and comparison keyRound-trip result
JoinCompare exact and policy-derived keysMatched, unmatched, or collision state
Sort or indexCheck ordering and lookup behaviorKey policy and locale assumptions
JSON canonicalizationConfirm strings are unchanged unless policy runs firstPre and post text

RFC 8785 canonicalizes JSON representation but preserves Unicode strings as-is. It can make serialized bytes deterministic without making canonically equivalent identifiers equal. Run the identifier policy before a comparison, and treat canonical serialization as a separate transport check Internet Engineering Task Force, (opens in a new tab).

Take a qualified next step

If the owner can describe the collection and its handling authority, the next step may be the appraisal route. That route is a request for review, not automatic acceptance, a quote, a buyer match, a legal opinion, or a license. Use synthetic Unicode pairs to test the policy first; do not send private names merely to demonstrate a normalization collision.

Takeaways

  • Preserve original text before deriving any comparison key.
  • Choose normalization and case rules by identifier role.
  • Use compatibility normalization only when its information loss is intended.
  • Detect collisions and hold ambiguous pairs for an owner decision.
  • Keep Unicode policy separate from JSON canonicalization and visual display.

Sources

  1. Unicode Consortium: Unicode Standard Annex #15: Unicode Normalization Forms (opens in a new tab)
  2. Unicode Consortium: Unicode Standard Annex #31: Unicode Identifier and Pattern Syntax (opens in a new tab)
  3. Internet Engineering Task Force: JSON Canonicalization Scheme (JCS) (opens in a new tab)

Use the appraisal route only after the identifier policy is documented.

Preparation does not promise acceptance, a buyer, a quote, or an offer.