Как экранировать строки JSON: символы, stringify и подводные камни
Экранировать строку JSON — значит превратить произвольный текст в строку, которую можно безопасно поместить внутрь JSON-документа как строковый литерал. Небольшой набор символов — двойная кавычка, обратный слеш и управляющие символы вроде перевода строки и табуляции — несёт структурный смысл или попросту недопустим внутри строки JSON, поэтому каждый из них заменяется безопасной escape-последовательностью, например \", \\ или \n. Сделаете это неправильно — и payload перестанет разбираться.
С этим сталкиваешься постоянно: вкладываешь один JSON-объект в другой как строковое поле, вставляешь многострочный фрагмент кода в значение конфигурации или вручную собираешь тело REST-запроса для curl. Ниже разберём, какие именно символы нужно экранировать, отделим экранирование от JSON.stringify, пройдёмся по вложенности JSON-в-JSON и экранированию Unicode и перечислим ошибки, которые тихо ломают payload. Если экранировать что-то нужно прямо сейчас, наш инструмент Экранирование JSON делает это в браузере — но прочитайте дальше, чтобы понять, почему он работает именно так.
Что такое экранирование строк JSON?
Экранирование строк JSON — это процесс преобразования сырой строки в форму, безопасную для встраивания внутрь JSON-документа. JSON резервирует небольшой набор символов со структурным смыслом: двойная кавычка " ограничивает строку, а обратный слеш \ начинает escape-последовательность. Кроме того, управляющие символы ниже U+0020 — переводы строки, табуляции, возвраты каретки — вообще не имеют права появляться буквально внутри строки JSON. Экранирование заменяет каждый из них безопасной последовательностью, чтобы итоговая строка разбиралась корректно где угодно.
Когда оно действительно нужно? Несколько ситуаций встречаются снова и снова:
- JSON-в-JSON: конверт webhook, сообщение Kafka или журнал аудита хранят тело запроса как строковое поле, поэтому внутренний JSON приходится экранировать, прежде чем его можно будет присвоить.
- Конфигурация, написанная вручную: чтобы поместить многострочный shell-скрипт, SQL-запрос или фрагмент кода в одно значение JSON, нужно превратить каждый перевод строки в
\n. - Тела REST-запросов: при ручной сборке тела JSON для
curlили HTTP-клиента, где кавычки и переводы строк должны пережить и оболочку, и передачу по сети. - Безопасное логирование: запись содержимого, предоставленного пользователем, в структурированную строку лога так, чтобы внедрённая кавычка или перевод строки не испортили формат.
Пара слов о порядке действий. Если вы начинаете с грязного или непроверенного JSON, сначала проверьте его, чтобы экранировать корректно сформированные данные — вставьте его в Форматировщик JSON, чтобы красиво отформатировать и проверить, и только потом экранируйте чистый результат. Экранирование мусора даёт лишь экранированный мусор.
Какие символы обязательно экранировать в JSON
Спецификация JSON определяет точный короткий список. Семь символов имеют выделенную двухсимвольную escape-последовательность, а всё остальное ниже U+0020 заменяется Unicode-экранированием \uXXXX. Вот полный набор символов экранирования JSON:
| Символ | Экранируется в | Примечания |
|---|---|---|
" (U+0022) | \" | Разделитель строки |
\ (U+005C) | \\ | Начало escape-последовательности (случай json escape backslash) |
| перевод строки (U+000A) | \n | |
| возврат каретки (U+000D) | \r | |
| табуляция (U+0009) | \t | |
| backspace (U+0008) | \b | |
| перевод страницы (U+000C) | \f | |
| прочие управляющие < U+0020 | \uXXXX | напр. U+0000 → \u0000 |
Что не требует экранирования — не менее важно. Прямой слеш / — совершенно обычный символ (его экранирование необязательно и полезно лишь в одном узком случае, разобранном ниже). Одинарные кавычки никогда не нуждаются в экранировании, потому что JSON не использует их как разделители. И каждый печатный символ от U+0020 и выше — включая все многобайтовые символы UTF-8, такие как é, 日 или 😀 — допустим как есть.
Вот эта разница на конкретном примере. Слева — сырой ввод, справа — экранированный строковый литерал JSON:
Input:
She said "hello" then left.
Escaped:
"She said \"hello\"\tthen left."
Двойные кавычки превратились в \", а табуляция — в \t. Теперь строку безопасно вставить в любой парсер JSON, строку лога или тело запроса.
JSON escape против JSON stringify: в чём разница?
Здесь многие путаются, а большинство руководств этот момент проскакивает. Экранирование и JSON.stringify — это не две разные операции, а одна и та же с двух сторон.
JSON.stringify(value) сериализует любое JavaScript-значение в его текстовое представление JSON. Когда этим значением оказывается строка, её сериализация означает обёртывание в двойные кавычки и экранирование специальных символов внутри. Это и есть экранирование JSON. Так что JSON.stringify("a\tb") возвращает строку из семи символов "a\tb", включая кавычки.
Практический вопрос — нужны ли вам эти внешние кавычки. Это напрямую соответствует опции Обернуть в двойные кавычки в инструменте Экранирование JSON:
| Режим | Вывод для ввода a"b | Когда использовать |
|---|---|---|
| Обёртка вкл | "a\"b" | Полноценный строковый литерал JSON, идентичный JSON.stringify. Присвойте его переменной или вставьте после двоеточия. |
| Обёртка выкл | a\"b | Только экранированное тело, без окружающих кавычек. Используйте, когда сами набираете кавычки в JSON-документе. |
Так что если вы искали «json stringify» и попали сюда, запомнить можно так: stringify строки — это и есть экранирование с включённой обёрткой. Форма без кавычек — то же самое, только внешние кавычки сняты.
Как экранировать строку для JSON в коде
Золотое правило: никогда не собирайте вручную цепочку вызовов replace(). В каждом основном языке есть сериализатор JSON, который корректно обрабатывает кавычки, обратные слеши, управляющие символы и Unicode. Используйте его.
JavaScript
const text = 'She said "hi"\nthen left.';
const escaped = JSON.stringify(text);
console.log(escaped);
// "She said \"hi\"\nthen left."
JSON.stringify для строки даёт полный литерал в кавычках. Нужно только тело? Отрежьте первый и последний символы: JSON.stringify(text).slice(1, -1).
Python
import json
text = 'She said "hi"\nthen left.'
print(json.dumps(text))
# "She said \"hi\"\nthen left."
print(json.dumps(text, ensure_ascii=False))
# "She said \"hi\"\nthen left." (non-ASCII kept as UTF-8)
json.dumps по умолчанию использует ensure_ascii=True, что экранирует каждый не-ASCII символ в \uXXXX — то же поведение, что и в ASCII-безопасном режиме инструмента. Передайте ensure_ascii=False, чтобы сохранить сырой UTF-8.
PHP
<?php
$text = "café \"quoted\"\nline";
echo json_encode($text);
// "caf\u00e9 \"quoted\"\nline" (default escapes non-ASCII to \uXXXX)
echo json_encode($text, JSON_UNESCAPED_UNICODE);
// "café \"quoted\"\nline"
json_encode по умолчанию экранирует и не-ASCII символы, и прямые слеши. Добавьте JSON_UNESCAPED_UNICODE, чтобы сохранить читаемость акцентов, и JSON_UNESCAPED_SLASHES, чтобы оставить / в покое.
Go и Java
В Go json.Marshal(text) возвращает экранированные байты в кавычках:
b, _ := json.Marshal(`a "quoted" line`)
// b == `"a \"quoted\" line"`
В Java objectMapper.writeValueAsString(text) из Jackson или JSONObject.quote(text) из org.json дают такой же литерал в кавычках. На каком бы языке вы ни писали, опирайтесь на библиотеку — она уже знает каждый граничный случай, о котором вы бы забыли.
Встраивание JSON внутрь JSON (JSON-в-JSON)
Это самая частая причина, по которой JSON экранируют вручную. Конверт webhook, запись очереди сообщений или журнал аудита нередко хранят целое тело запроса как строковое поле. Чтобы это сделать, внутренний JSON сначала нужно экранировать.
Проследите, как небольшой объект проходит через два слоя кодирования:
1. Inner object: {"a":1}
2. Escaped as a string: "{\"a\":1}"
3. Placed in envelope: {"payload": "{\"a\":1}"}
Каждая " во внутреннем объекте стала \", а всё целиком обёрнуто во внешнюю пару кавычек. Результат — единое корректное строковое значение, которое можно присвоить полю payload.
Подвох с более глубокой вложенностью в том, что обратные слеши размножаются. Экранирование уже экранированной строки экранирует и её обратные слеши, поэтому каждый слой примерно удваивает их: внутренняя кавычка, бывшая \", на уровень выше становится \\\", а ещё на уровень выше — \\\\\". Трёхуровневый JSON-в-JSON читать по-настоящему трудно, и именно поэтому помогает инструмент. Чтобы пойти в обратную сторону и вытащить внутренний объект обратно из строки, прогоните её через наш инструмент Деэкранирование JSON.
Unicode и экранирование \uXXXX
По умолчанию JSON прекрасно работает с сырым UTF-8. é остаётся é, 日 остаётся 日, и документ от этого только читабельнее. Экранировать какой-либо печатный символ Unicode не нужно.
Так когда же стоит обращаться к ASCII-безопасному выводу \uXXXX? Только когда нижестоящей системе нельзя доверить UTF-8: старые шлюзы SOAP или XML, некоторые конвейеры логирования, заголовки писем или исходные файлы, которые должны оставаться чистым ASCII. В ASCII-безопасном режиме каждый символ выше U+007F становится escape-последовательностью \uXXXX — café превращается в caf\u00e9. Это шумнее, но побайтово ASCII, и декодируется обратно в оригинал любым совместимым парсером.
Есть одна тонкость. \uXXXX кодирует одну 16-битную кодовую единицу UTF-16, но символам за пределами Basic Multilingual Plane — эмодзи, редким письменностям — нужен 21 бит. JSON обрабатывает их через суррогатную пару: две escape-последовательности \uXXXX подряд. Улыбающееся лицо 😀 (U+1F600) становится \ud83d\ude00. Большинство сериализаторов делают это за вас; опасность — в написанном вручную экранировщике, который выдаёт одиночный непарный суррогат.
Если суррогатные пары и кодовые точки для вас новая территория, Гид UTF-8 vs UTF-16 vs Unicode подробно разбирает, как один символ отображается в байты и кодовые единицы. Это недостающий контекст, объясняющий, почему одному эмодзи нужны две escape-последовательности.
Деэкранирование: чтение экранированного JSON обратно
У экранирования есть обратная операция. Чтобы превратить "a\tb" обратно в настоящий текст с переводом строки или табуляцией, его разбирают: JSON.parse(str) в JavaScript, json.loads(str) в Python. Парсер проходит по каждой escape-последовательности и восстанавливает исходные символы, включая суррогатные пары.
Когда деэкранирование не удаётся, ошибка почти всегда — «invalid escape sequence», и у неё есть несколько типичных причин:
- Одиночный обратный слеш перед символом, который JSON не распознаёт как escape, например
\q. - Выдуманная escape-последовательность вроде
\x41— в JSON нет шестнадцатеричного escape\x; используется только\u. - Обрезанная escape-последовательность
\uменее чем с четырьмя шестнадцатеричными цифрами, например\u00. - Лишняя или непарная двойная кавычка, ломающая границу строки.
Проверьте, что каждый обратный слеш начинает одну из допустимых escape-последовательностей (\n \r \t \b \f \" \\ \/ \uXXXX) и что кавычки парны. Для экранированных строк, скопированных из середины строки лога — где внешние кавычки остались позади — наш инструмент Деэкранирование JSON принимает тело с окружающими кавычками или без них и декодирует его в любом случае.
Частые подводные камни при экранировании JSON
Большинство сломанных payload сводятся к одной из этих шести ошибок.
1. Двойное экранирование. Экранирование текста, который уже был экранирован, превращает \n в \\n, а \" в \\\", и потребитель читает буквальный «обратный слеш и n» вместо перевода строки. Обычно это случается, когда вышестоящий сервис уже экранировал значение в JSON, а вы экранируете его ещё раз. Сначала деэкранируйте, чтобы проверить текущее состояние, затем экранируйте ровно один раз.
2. Забытые внешние кавычки. С выключенной обёрткой вы получаете только экранированное тело, а не полноценную строку. Вставка hello \"world\" прямо туда, где ожидается значение JSON, недопустима, потому что окружающие кавычки отсутствуют. Либо оставьте обёртку включённой, либо наберите кавычки сами.
3. Излишнее экранирование не-ASCII. Включение ASCII-безопасного режима, когда потребитель прекрасно справляется с UTF-8, просто раздувает вывод. café становится caf\u00e9 без всякой причины — труднее читать, больше на проводе, никакой пользы. Оставьте режим выключенным, если только конкретная устаревшая система не требует чистого ASCII.
4. Экранирование прямого слеша по привычке. Escape / важен ровно в одном месте: JSON, встроенный внутрь HTML-тега <script>, где подстрока </script> закрыла бы тег раньше времени независимо от контекста JSON. Экранирование / в \/ нейтрализует это. Вне этого единственного случая экранирование слешей — чистый мусор; оставьте его выключенным для тел REST, файлов конфигурации и payload сообщений.
5. Самодельные цепочки replace. Ручной конвейер replace('"', '\\"') почти всегда что-нибудь забывает — управляющий символ, backspace, суррогатную пару. Используйте сериализатор языка, который покрывает всю спецификацию.
6. Экранирование без деэкранирования (или двойное деэкранирование). Круговой обход должен сходиться. Экранируйте один раз на входе, деэкранируйте один раз на выходе. Деэкранируете дважды — и калечите настоящие обратные слеши, которые были частью данных.
Ещё одно различие, которое стоит зафиксировать: экранирование JSON — это не URL- и не процентное кодирование. Они решают разные задачи для разных транспортов, и их смешивание — процентное кодирование значения с последующим экранированием результата в JSON или наоборот — даёт месиво, которое ни один парсер не прочитает корректно. Руководство по URL-кодированию и декодированию объясняет, когда процентное кодирование — правильный инструмент и чем оно отличается от того, что делает JSON.
Часто задаваемые вопросы
Что значит экранировать строку в JSON?
Это значит заменить символы, несущие для JSON структурный смысл — двойную кавычку, обратный слеш и управляющие символы вроде перевода строки и табуляции — безопасными escape-последовательностями, такими как \", \\ и \n. Результат можно встроить как строковый литерал внутрь JSON-документа, не ломая разбор.
Какие символы нужно экранировать в JSON?
Двойная кавычка, обратный слеш, перевод строки, возврат каретки, табуляция, backspace и перевод страницы получают каждый свою выделенную escape-последовательность, а любой другой управляющий символ ниже U+0020 становится \uXXXX. Печатные символы и многобайтовый UTF-8 не требуют экранирования; прямой слеш необязателен и важен только внутри HTML-тегов <script>.
JSON escape — это то же самое, что JSON.stringify?
По большей части это два взгляда на одну операцию. JSON.stringify, применённый к строке, оборачивает её в двойные кавычки и экранирует специальные символы внутри — это и есть экранирование JSON. Обёртка включена равна форме в кавычках (идентичной JSON.stringify); обёртка выключена даёт только экранированное тело без окружающих кавычек.
Как экранировать строку для JSON в JavaScript или Python?
В JavaScript используйте JSON.stringify(str); в Python — json.dumps(str). Всегда полагайтесь на встроенную функцию, а не на написанную вручную цепочку replace — встроенные функции корректно обрабатывают Unicode, управляющие символы и каждый граничный случай, который вы иначе упустили бы.
Почему мой JSON ломается из-за лишних обратных слешей?
Обычная причина — двойное экранирование: экранирование текста, который уже был экранирован, так что \n становится \\n, и потребитель читает буквальный «обратный слеш и n» вместо перевода строки. Сначала деэкранируйте значение, чтобы проверить его настоящее состояние, затем экранируйте ровно один раз.
Нужно ли экранировать прямые слеши или Unicode в JSON?
Ни то, ни другое не требуется. / — обычный символ, и его нужно экранировать, только когда вы встраиваете JSON в HTML-тег <script>, чтобы последовательность </script> не закрыла его раньше времени. Unicode по умолчанию остаётся сырым UTF-8; используйте \uXXXX только когда нижестоящая система не справляется с UTF-8.