Экранирование и деэкранирование в 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 |
\b | Backspace (забой) | U+0008 |
\f | Form feed (прогон страницы) | U+000C |
\n | Перевод строки (newline) | U+000A |
\r | Возврат каретки | U+000D |
\t | Горизонтальная табуляция | U+0009 |
\uXXXX | Unicode-символ (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 oneline 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 jsondata = {"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, '"')}"`;
Практические советы по экранированию
-
Всегда используйте библиотечный сериализатор — никогда не стройте JSON-строки конкатенацией. В каждом основном языке есть стандартная JSON-библиотека, которая корректно выполняет экранирование.
-
Проверяйте уровень кодирования — когда значение выглядит двойно экранированным, проследите каждый уровень сериализации и десериализации, чтобы найти место, где экранирование применяется более одного раза.
-
Используйте сырые строковые литералы для путей — в Python используйте сырые строки (
r"C:\Users\alice") или прямые слеши ("C:/Users/alice"), чтобы избежать случайных escape-последовательностей в путях Windows. -
Валидируйте после ручного конструирования — если вам необходимо создать JSON вручную (например, в шелл-скрипте), перед использованием проверьте результат парсером.
-
Явно указывайте кодировку — при сериализации в файл явно задавайте кодировку UTF-8, а не полагайтесь на системное умолчание, чтобы Unicode-символы обрабатывались согласованно.
Использование инструмента экранирования/деэкранирования
Иногда вы получаете JSON-строку, которая была сериализована дважды — JSON-значение, которое само является кавычечной, экранированной JSON-строкой — и нужно деэкранировать её до читаемой формы. Или у вас есть необработанная строка со специальными символами, которую нужно безопасно встроить в JSON-документ.
Инструмент экранирования/деэкранирования выполняет эти преобразования мгновенно, без написания кода парсинга.
Инструмент Escape на JSONKit позволяет вставить необработанный текст и получить корректно экранированное JSON-строковое представление, или вставить экранированную JSON-строку и получить деэкранированный оригинал. Вся обработка происходит в браузере — данные не отправляются ни на какой сервер.
Заключение
Экранирование JSON автоматически обрабатывается каждой стандартной JSON-библиотекой — это самый важный вывод: используйте JSON.stringify / json.dumps / JsonSerializer.Serialize и позвольте библиотеке выполнять экранирование. Ручное конструирование JSON — корневая причина большинства ошибок экранирования.
Когда проблемы с экранированием всё же возникают — двойное экранирование, необработанные управляющие символы, путаница с уровнями кодирования — систематически проследите каждый шаг сериализации. Справочник по escape-последовательностям в этом руководстве охватывает каждую последовательность, определённую спецификацией JSON.