Веб-форма на сервере принимала имя устройства, и кто-то честно вписал корпус «A» с кавычками — пакет ушёл в плату, разбор молча сорвался, полчаса ушли на поиск причины в железе, которое было ни при чём. Внутри строки JSON кавычка — служебный символ: она закрывает значение, и всё после неё парсер читает как синтаксис. Лечится это одним знаком — обратным слэшем, но между «знаю правило» и «вижу, как оно работает на двух слоях сразу» — дистанция, на которой я и потерял те полчаса.
Урок про JSON-синтаксис показал грамматику формата, здесь разбираем единственный тип значения, где грамматика кусается: строки. Шесть экранирующих последовательностей, двойной слой экранов C++ и JSON, кириллица в UTF-8 и её вес в байтах, и перечень того, что реально срывает deserializeJson с кодом вроде InvalidInput. Всё это проверяется в мониторе за десять минут — и после этого сломанные строки перестают быть загадкой.
Про сроки жизни строк и копии в пуле документа мы говорили в отдельном уроке; здесь — только про содержимое строк: какие символы, в каком виде и сколько байт.
Шесть последовательностей, которые надо знать
Список короткий: \" — кавычка, \\ — сам обратный слэш, \n — перенос строки, \r — возврат каретки, \t — табуляция, \uXXXX — символ по шестнадцатеричному коду. Всё прочее внутрь строкового значения попадает как есть. Правило одно: обратный слэш — переключатель режима, после него следующий символ читается не как синтаксис, а как содержимое. Хотите настоящий слэш в значении — удвойте его.
Стороны у формата две. На выходе сериализатор ставит экраны сам: присвойте doc.name = "korf \"A\"" — и serializeJson напечатает значение с кавычкой и экраном, руками ничего добавлять не нужно. Боль живёт на входе — в строках, которые вы собираете вручную или принимаете: там экраны обязан поставить отправитель. И тут вступает второй слой: C++ сам обрабатывает обратный слэш в своих литералах, поэтому в исходнике скетча " превращается в \", а чтобы в строке остался JSON-экран, пишут \\\". Два слоя экранов — место, где путаются все, включая меня.
От второго слоя лечат raw-литералом: R"json({...})json" — между ограждениями json( и )json компилятор ничего не экранирует, и остаётся один слой, JSON-овский. В примерах официальной документации библиотеки этот приём стандартный, и я перешёл на него полностью: читать пакет в скетче, где не надо считать слэши, — отдельное удовольствие.
#include <ArduinoJson.h> DynamicJsonDocument doc(768); // v7: JsonDocument doc; void setup() { Serial.begin(9600); // Кавычка внутри значения: экран ставим в JSON-строке сами, // в C++-литерале он удваивается DeserializationError e1 = deserializeJson(doc, "{\"name\":\"korf \\\"A\\\"\"}"); if (e1) { Serial.println(e1.c_str()); return; } Serial.print("name="); Serial.println((const char*)doc["name"]); // Перенос: JSON-экран, а не настоящий байт 0x0A внутри значения DeserializationError e2 = deserializeJson(doc, "{\"msg\":\"pervaya\\nvtoraya\"}"); if (e2) { Serial.println(e2.c_str()); return; } Serial.println((const char*)doc["msg"]); // Raw-литерал: один слой экрана вместо двух DeserializationError e3 = deserializeJson(doc, R"json({"device":"nasos","place":"podval"})json"); if (e3) { Serial.println(e3.c_str()); return; } // Обратная печать: сериализатор ставит экраны сам serializeJson(doc, Serial); Serial.println(); Serial.print("bayt="); Serial.println(measureJson(doc));} void loop() {}Кириллица — отдельная история с приятным финалом. JSON-строки по спецификации — Unicode, и UTF-8 в них разрешён как есть: сериализатор ArduinoJson не перекодирует байты и не экранирует не-ASCII, значение едет в пакет байт в байт. Плата за это — счёт: в UTF-8 каждая кириллическая буква весит два байта. Значение «Привет, мир» — двадцать байт при одиннадцати буквах на глаз; measureJson вернёт именно байты, а не буквы — при расчёте буферов и трафика это частая подножка.
Второе условие — канал и терминал. Монитор порта должен стоять в UTF-8, иначе в окне будет каша из двухбайтовых последовательностей, прочитанных по одному байту. Экран \u041f — та же буква П, но в ASCII-упаковке: шесть безопасных байт вместо двух. Его смысл — совместимость: канал, пропускающий только семь бит, довезёт escape и не довезёт сырой UTF-8. В моих проектах канал почти всегда восьмибитный, поэтому я отправляю сырую кириллицу, а escape держу в запасе для чужих API с капризами.
Декодер \uXXXX в библиотеке можно и выключить: директива ARDUINOJSON_DECODE_UNI в ноль перед include выбрасывает его из сборки. На AVR экономия флеша заметная, и если входящие пакеты заведомо ASCII — честная сделка. Я оставлял включённым: половина сервисов отдаёт русские тексты именно escape-записью.
Что реально срывает разбор
Перечень короткий, и он не про экзотику. Незакрытая кавычка — значение уезжает в никуда, парсер возвращает InvalidInput. Настоящий байт перевода строки внутри строкового значения — по спецификации запрещён, нужен экран \n: многострочный текст из текстового поля формы без экранирования срывает пакет гарантированно. \u без четырёх шестнадцатеричных цифр после себя — InvalidInput. Обрыв пакета на середине — IncompleteInput: строка кончилась раньше, чем синтаксис. Имена кодов печатаются err.c_str() — мы собирали весь разбор в уроке про диагностику, и печатать код при срыве — первая привычка, которую я ставлю в каждый скетч с чужим JSON.
Отдельный случай — приём по UART: если вы собираете пакет посимвольно до разделителя, незакрытая кавычка внутри значения может съесть ваш разделитель, и пакет срастётся с соседним. Разбор потока и границы сообщений — тема урока про JSON в UART; здесь зафиксируйте главное: сломанная строка ломает не только своё поле, но и границы пакетов вокруг.
Про эмодзи скажу с оговоркой: они кодируются парой \uXXXX-записей, и поведение таких пар зависит от версии библиотеки — в моих сборках попадались буквы-огрызки вместо смайлов. Проверяйте на своей версии, если без них никак; я просто не тащу эмодзи в пакеты микроконтроллеров — тексты и числа перевозят всё нужное.
| Экран | Вставляет в значение | Пример |
|---|---|---|
| \" | Кавычку | "name":"korf \"A\"" — имя с кавычками |
| \\ | Обратный слэш | путь C:\\temp в значении |
| \n | Перевод строки | сообщение из двух строк в одном поле |
| \t | Табуляцию | выравнивание в логе |
| \uXXXX | Символ по коду | \u041f — это П, шесть ASCII-байт в пакете |
Симулятор ниже прогоняет три разбора подряд: кавычка внутри значения, перенос, кириллица escape-записью. Светодиод загорается, только если все три распались корректно — по сути это мини-тест строкового слоя, и на настоящей плате такой тест стоит добавить в setup любого проекта с чужими пакетами. Под разборами — обратная печать последнего документа: видно, как сериализатор возвращает экраны на место, и счётчик байт для значения с кириллицей.
Одна оговорка про счётчик: движок книги считает символы, настоящая библиотека — байты UTF-8, и на кириллице цифры разойдутся на число кириллических букв. Оба числа полезны: первое — длина в мониторе, второе — правда для буфера и канала. В уроке про строки и копии мы разбирали, куда ложится само значение, а в седьмой версии пул копирует строки сам — об этом урок про JsonDocument.
- 1. В значении поля нужна кавычка: имя korf "A". Как записать это в JSON?
- 2. Сериализатор получил в doc.name строку korf "A" и печатает serializeJson. Что уйдёт в канал?
- 3. Сколько байт займёт значение «Привет, мир» в пакете на настоящей плате?
- 4. Пакет оборвался на середине из-за шума в линии. Что вернёт deserializeJson?
- 5. Почему \u041f и сырая буква П — один символ, но разный вес в канале?
- Шесть экранов наизусть: кавычка, слэш, перенос, возврат каретки, табуляция, \uXXXX — остальное едет как есть
- На выходе сериализатор экранирует сам; боль — на входе, в ручных строках и на слое C++
- Два слоя экранов лечатся raw-литералом R"json(...)json": остаётся один слой, JSON-овский
- Кириллица в UTF-8 — два байта на букву; measureJson возвращает байты, монитор держите в UTF-8
- Обрыв и кривой синтаксис дают разные коды: IncompleteInput против InvalidInput — печатайте err.c_str()
Частые вопросы по теме урока
- В значении поля нужна кавычка: имя korf "A". Как записать это в JSON?
- Кавычка внутри строкового значения экранируется обратным слэшем: он переключает парсер с режима синтаксиса на режим содержимого. Одинарные кавычки в JSON не работают вовсе. "name":"korf \"A\"" — перед внутренними кавычками обратный слэш
- Сериализатор получил в doc.name строку korf "A" и печатает serializeJson. Что уйдёт в канал?
- На выходе сериализатор экранирует сам: обратная печать значения с кавычкой всегда валидна. Боль — только на входе, в строках, собранных руками или пришедших извне. "name":"korf \"A\"" — экраны поставлены автоматически
- Сколько байт займёт значение «Привет, мир» в пакете на настоящей плате?
- В UTF-8 кириллическая буква — два байта: шесть букв дают двенадцать, запятая и пробел по одному, три латинские буквы по одному — двадцать. measureJson вернёт байты, не буквы. 20: шесть кириллических букв по два байта, остальное по одному
- Пакет оборвался на середине из-за шума в линии. Что вернёт deserializeJson?
- Обрыв даёт код вроде IncompleteInput, сломанный синтаксис — InvalidInput. Печатайте err.c_str(): имя кода сразу говорит, искать ли усечённый приём или кривую строку. Код вроде IncompleteInput — разбора не было
- Почему \u041f и сырая буква П — один символ, но разный вес в канале?
- Escape-запись — шесть безопасных ASCII-символов, она доезжает даже по каналу с семью битами. Сырая кириллица — два байта UTF-8, но канал обязан пропускать байты выше 127. Плата за совместимость против трафика. Escape занимает шесть ASCII-байт, сырой UTF-8 — два байта