Блог

Экранирование и деэкранирование в JSON: полное руководство

Экранирование — одна из тех тем, с которой рано или поздно сталкивается каждый разработчик, обычно когда что-то ломается непредсказуемым и неочевидным способом. JSON-строка, которая выглядит корректно, отвергается парсером; строка, которая должна содержать перевод строки, отображает буквальный \n; значение, которое хранилось корректно, возвращается искажённым после передачи через API. Почти каждая из этих проблем сводится к экранированию. В этом руководстве объясняется, зачем нужно экранирование, какие символы его требуют и как правильно с ним работать в разных языках и контекстах.

Зачем нужно экранирование

JSON-строки ограничены символами двойных кавычек. Это сразу создаёт проблему: что если сама строка содержит двойную кавычку? Без экранирования парсер воспримет внутреннюю кавычку как конец строки.

// Неверно — внутренняя кавычка преждевременно завершает строку
{ "message": "She said "hello" and left" }
// Верно — внутренние кавычки экранированы
{ "message": "She said \"hello\" and left" }

Помимо двойных кавычек, другие символы требуют экранирования по структурным или техническим причинам:

  • Управляющие символы — такие как перевод строки, табуляция и возврат каретки — нарушили бы однострочное представление значения JSON
  • Сам обратный слеш является символом экранирования, поэтому буквальный обратный слеш должен быть экранирован, чтобы отличить его от escape-последовательности
  • Не-ASCII Unicode можно экранировать для безопасной передачи через системы, которые могут некорректно обрабатывать Unicode

Справочник по escape-последовательностям JSON

Escape-последовательностьСимволКод Unicode
"Двойная кавычкаU+0022
\Обратный слешU+005C
/Прямой слеш (необязательно)U+002F
\bBackspace (забой)U+0008
\fForm feed (прогон страницы)U+000C
\nПеревод строки (newline)U+000A
\rВозврат кареткиU+000D
\tГоризонтальная табуляцияU+0009
\uXXXXUnicode-символ (4 шестнадцатеричные цифры)Любая кодовая точка BMP

Обратите внимание, что прямой слеш (/) экранируется опционально — \/ и / оба валидны в JSON. Это исторически обусловлено необходимостью встраивания JSON внутрь HTML-тегов <script>, где </ завершало бы блок скрипта.

Экранирование Unicode

Любой Unicode-символ можно представить в JSON с помощью escape-последовательности \uXXXX, где XXXX — это четырёхзначная шестнадцатеричная кодовая точка.

{
"greeting": "\u0048\u0065\u006C\u006C\u006F",
"copyright": "\u00A9 2024",
"checkmark": "\u2713"
}

После парсинга "\u0048\u0065\u006C\u006C\u006F" становится строкой "Hello". Экранирование Unicode полезно, когда нужно убедиться, что JSON-документ содержит только ASCII-символы для передачи через системы, которые могут искажать символы с высокими байтами.

Суррогатные пары для символов выше U+FFFF

Символы за пределами Basic Multilingual Plane (кодовые точки выше U+FFFF) — такие как эмодзи и редкие CJK-символы — требуют суррогатных пар в нотации \uXXXX формата JSON.

{
"emoji": "\uD83D\uDE00"
}

Две последовательности \u вместе представляют единственный символ 😀 (U+1F600). Большинство JSON-сериализаторов обрабатывают это автоматически, но если вы конструируете JSON вручную, помните, что одна старшая кодовая точка требует двух escape-последовательностей \u.

Современные JSON-парсеры также принимают символ в кодировке UTF-8 непосредственно в строке, поэтому "emoji": "😀" является валидным JSON при условии, что файл закодирован в UTF-8.

Распространённые ошибки при экранировании

Двойное экранирование

Двойное экранирование происходит, когда вы экранируете строку, которая уже экранирована, или когда экранирование применяется на нескольких уровнях системы. Классический пример — хранение JSON внутри поля JSON-строки.

// Внешний JSON со значением, которое само является JSON-строкой
{
"payload": "{\"key\": \"value\"}"
}

Если система, получающая это значение, затем снова экранирует его перед сохранением, обратные слеши экранируются:

{
"payload": "{\\\"key\\\": \\\"value\\\"}"
}

После двух итераций деэкранирования вы получите исходное значение, но после одной — сломанную строку. Решение — экранировать ровно на одном уровне и деэкранировать ровно на одном уровне. Никогда не экранируйте уже экранированное значение.

Путаница JSON-экранирования с URL-кодированием

JSON-экранирование и URL-кодирование (percent-encoding) — совершенно разные механизмы. Пробел — это %20 в URL и " " (экранирование не нужно) в JSON-строке. Прямой слеш — это %2F в сегменте пути URL и / (или опционально \/) в JSON.

Применение URL-кодирования к значению перед его встраиванием в JSON порождает двойное кодирование:

// Неверно: URL-кодирование значения перед JSON-сериализацией
const value = 'hello world';
const wrong = JSON.stringify(encodeURIComponent(value));
// Результат: "\"hello%20world\"" — %20 является буквальным текстом в JSON
// Верно: позвольте JSON.stringify выполнить экранирование
const correct = JSON.stringify(value);
// Результат: "\"hello world\"" — пробел валиден в JSON-строках

Неэкранированные управляющие символы

JSON требует экранирования управляющих символов (U+0000 — U+001F). Необработанный перевод строки или символ табуляции, встроенный непосредственно в JSON-строку, технически невалиден, хотя некоторые парсеры это допускают.

// Неверно — необработанный перевод строки в значении строки
{ "note": "line one
line two" }
// Верно — экранированный перевод строки
{ "note": "line one\nline two" }

Экранирование в различных языках

JavaScript

JSON.stringify() в JavaScript автоматически выполняет всё необходимое экранирование. Никогда не конструируйте JSON-строки вручную в JavaScript.

const data = {
message: 'She said "hello"',
path: 'C:\\Users\\alice',
multiline: 'line one\nline two',
};
const json = JSON.stringify(data, null, 2);
// Автоматически производит корректно экранированный JSON

JSON.parse() также автоматически выполняет деэкранирование:

const parsed = JSON.parse('{"path": "C:\\\\Users\\\\alice"}');
console.log(parsed.path); // C:\Users\alice

Обратите внимание на дополнительный уровень экранирования в строковом литерале JavaScript: "\\\\" — это четырёхсимвольная строка JavaScript, содержащая два обратных слеша, которая представляет один экранированный обратный слеш в JSON, который разбирается как один обратный слеш.

Python

Модуль json в Python работает так же — используйте json.dumps() и json.loads(), никогда не конструируйте JSON вручную.

import json
data = {
"message": 'She said "hello"',
"path": r"C:\Users\alice",
"multiline": "line one\nline two"
}
json_string = json.dumps(data, ensure_ascii=False)
# ensure_ascii=False сохраняет Unicode-символы как есть,
# а не экранирует их в последовательности \uXXXX
# Деэкранирование
parsed = json.loads('{"path": "C:\\\\Users\\\\alice"}')
print(parsed["path"]) # C:\Users\alice

Параметр ensure_ascii=False стоит запомнить: по умолчанию json.dumps() в Python экранирует все не-ASCII символы в последовательности \uXXXX. Установка ensure_ascii=False производит более читаемый вывод, если вы уверены, что целевая система корректно обрабатывает UTF-8.

Экранирование в шаблонных строках

При встраивании JSON в HTML, SQL или шелл-скрипты нужно экранировать как для JSON, так и для внешнего контекста. Самый безопасный подход — использовать механизм экранирования внешнего языка для уже сериализованного JSON:

// Встраивание JSON в HTML data-атрибут
const jsonValue = JSON.stringify({ key: 'value with "quotes"' });
const htmlAttribute = `data-config="${jsonValue.replace(/"/g, '&quot;')}"`;

Практические советы по экранированию

  1. Всегда используйте библиотечный сериализатор — никогда не стройте JSON-строки конкатенацией. В каждом основном языке есть стандартная JSON-библиотека, которая корректно выполняет экранирование.

  2. Проверяйте уровень кодирования — когда значение выглядит двойно экранированным, проследите каждый уровень сериализации и десериализации, чтобы найти место, где экранирование применяется более одного раза.

  3. Используйте сырые строковые литералы для путей — в Python используйте сырые строки (r"C:\Users\alice") или прямые слеши ("C:/Users/alice"), чтобы избежать случайных escape-последовательностей в путях Windows.

  4. Валидируйте после ручного конструирования — если вам необходимо создать JSON вручную (например, в шелл-скрипте), перед использованием проверьте результат парсером.

  5. Явно указывайте кодировку — при сериализации в файл явно задавайте кодировку UTF-8, а не полагайтесь на системное умолчание, чтобы Unicode-символы обрабатывались согласованно.

Использование инструмента экранирования/деэкранирования

Иногда вы получаете JSON-строку, которая была сериализована дважды — JSON-значение, которое само является кавычечной, экранированной JSON-строкой — и нужно деэкранировать её до читаемой формы. Или у вас есть необработанная строка со специальными символами, которую нужно безопасно встроить в JSON-документ.

Инструмент экранирования/деэкранирования выполняет эти преобразования мгновенно, без написания кода парсинга.

Инструмент Escape на JSONKit позволяет вставить необработанный текст и получить корректно экранированное JSON-строковое представление, или вставить экранированную JSON-строку и получить деэкранированный оригинал. Вся обработка происходит в браузере — данные не отправляются ни на какой сервер.

Заключение

Экранирование JSON автоматически обрабатывается каждой стандартной JSON-библиотекой — это самый важный вывод: используйте JSON.stringify / json.dumps / JsonSerializer.Serialize и позвольте библиотеке выполнять экранирование. Ручное конструирование JSON — корневая причина большинства ошибок экранирования.

Когда проблемы с экранированием всё же возникают — двойное экранирование, необработанные управляющие символы, путаница с уровнями кодирования — систематически проследите каждый шаг сериализации. Справочник по escape-последовательностям в этом руководстве охватывает каждую последовательность, определённую спецификацией JSON.