Řetězce v JSON: escapování a Unicode

Řetězce jsou v JSON nejčastější hodnota a zároveň nejčastější zdroj chyb: neescapovaná uvozovka, zpětné lomítko v cestě nebo skutečný konec řádku uvnitř textu. Kapitola ukazuje, co se escapovat musí, jak funguje zápis \uXXXX a co dělají parsery s neplatnými znaky.

Autor: Aktualizováno

Co se v řetězci musí escapovat

Řetězec je text ve dvojitých uvozovkách. Smí obsahovat jakýkoli znak Unicode kromě tří skupin, které se escapovat musí:

  • uvozovku ", jinak by řetězec skončil,
  • zpětné lomítko \, protože začíná escape sekvenci,
  • řídicí znaky U+0000 až U+001F, tedy hlavně konec řádku a tabulátor.

Všechno ostatní, včetně české diakritiky a emoji, může stát v UTF-8 přímo. Escapovat se smí libovolný znak, nutné to ale není.

Escape sekvence

JSON zná osm dvouznakových sekvencí a obecný zápis \uXXXX:

ZápisZnakVýznam
\""uvozovka
\\\zpětné lomítko
\//lomítko (escapovat se nemusí)
\bU+0008backspace
\fU+000Cform feed
\nU+000Anový řádek
\rU+000Dnávrat vozíku
\tU+0009tabulátor
\u00E9élibovolný znak ze základní roviny Unicode
\uD83D\uDE00😀znak mimo základní rovinu: náhradní pár UTF-16

Za \u následují přesně čtyři šestnáctkové číslice s kódem znaku; velikost písmen nehraje roli (\u00e9 i \u00E9). Jiné sekvence, které znáš z programovacích jazyků, JSON nepovoluje: \x41, \' ani \0.

Emoji a znaky mimo základní rovinu

Čtyři šestnáctkové číslice stačí jen na znaky do U+FFFF. Znak s vyšším kódem, například většina emoji, se escapuje jako náhradní pár UTF-16: dvě sekvence \u za sebou. Oba zápisy v ukázce znamenají stejný znak 😀 (U+1F600):

emoji.json
{
  "primo": "😀",
  "escapovane": "\uD83D\uDE00"
}

Pozor na délku: JavaScript počítá v jednotkách UTF-16, takže "😀".length je 2. Python i PHP s mb_strlen("😀", "UTF-8") vrátí 1, v UTF-8 má znak 4 bajty. Limit „nejvýš 100 znaků“ proto může v každém jazyce znamenat něco jiného.

Časté chyby v řetězcích

Ukázky ověřené parserem (⏎ značí skutečný konec řádku, ⇥ skutečný tabulátor):

ZápisPlatný JSON?PročNástroj
"řádek 1⏎řádek 2"neskutečný konec řádku uvnitř řetězce, správně \nVyzkoušet
"a⇥b"neskutečný tabulátor, správně \tVyzkoušet
"C:\Users"ne\U není platná escape sekvence, správně "C:\\Users"Vyzkoušet
"\x41"neJSON nezná \x, správně \u0041Vyzkoušet
'text'nejednoduché uvozovkyVyzkoušet
"C:\\Users"anozpětné lomítko zdvojenéVyzkoušet
"řádek 1\nřádek 2"anokonec řádku jako \nVyzkoušet

Nejčastější příčinou je skládání JSON z kousků textu. Jakmile data obsahují uvozovku nebo zpětné lomítko, výsledek přestane být platný. Řešením je vždy knihovna:

Chybně
// skládání JSON z řetězců: jedna uvozovka v datech a výsledek je neplatný
const jmeno = 'Pavel "Pája" Novák';
const json = '{"jmeno": "' + jmeno + '"}';
JSON.parse(json);   // SyntaxError
Správně
// knihovna escapuje sama
const json = JSON.stringify({ jmeno: 'Pavel "Pája" Novák' });
// '{"jmeno":"Pavel \\"Pája\\" Novák"}'
JSON.parse(json).jmeno;   // 'Pavel "Pája" Novák'

Jak řetězce zapisují knihovny

Výstup různých knihoven se liší, i když je ve všech případech platný. Python a PHP ve výchozím nastavení escapují všechny znaky mimo ASCII a PHP navíc i lomítko:

JazykVýchozí výstup pro "é" a "/"Jak to změnit
JavaScriptJSON.stringify: "é" a "/" beze změny—
Pythonjson.dumps: "\u00e9", lomítko beze změnyensure_ascii=False
PHPjson_encode: "\u00e9" a "\/"JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES

Na významu dat to nic nemění, parser z "é" i "\u00e9" udělá stejný řetězec. Escapování mimo ASCII ale zbytečně zvětšuje soubory s češtinou. Escapované lomítko má jeden praktický účinek: řetězec </script> v datech se zapíše jako <\/script>, takže neukončí skript, když JSON vložíš přímo do HTML.

Osamocené náhradní znaky

Gramatika JSON dovoluje i zápis, který žádný znak Unicode nekóduje: samotnou polovinu náhradního páru, třeba "\uDEAD". Vzniká, když program zkrátí text v UTF-16 uprostřed emoji. RFC 8259 chování parserů v tomto případě označuje za nepředvídatelné. Změřené chování (Node.js 25, Python 3.14 a PHP 8.4):

VstupJavaScriptPythonPHP
"\uDEAD"řetězec délky 1'\udead'chyba

Profil I-JSON (RFC 7493) takové řetězce zakazuje. Text proto zkracuj po celých znacích, ne po jednotkách UTF-16; v JavaScriptu například přes [...text].slice(0, 100).join('').

Porovnávání řetězců

Stejný řetězec lze zapsat různě: "a\\b" a "a\u005Cb" jsou po zpracování totožné. Řetězce (i názvy v objektech) proto porovnávej až po dekódování parserem, nikdy v surovém textu JSON. Pozor také na normalizaci Unicode: „é“ může být jeden znak U+00E9, nebo „e“ a kombinující čárka. JSON je nijak nesjednocuje a porovnání znak po znaku je vyhodnotí jako různé.