Погодный сервис обновился — и экран моей метеостанции обнулился. В логе пакет выглядел почти обычно: {"temp":"21.4","humi":58}. Первым делом я грешил на датчик, вторым — на сеть, и только через полчаса увидел кавычки: температура пришла строкой. Разбор прошёл успешно, ключ на месте, сервер отвечает — а float t = doc["temp"] вернул ноль: строка в число сама не превращается. Одна кавычка, которую в журнале глаз пропускает, стоила мне вечера и одной перепрошивки вслепую.
Скалярных типов в JSON четыре: строка, число, true/false и null; всё остальное — структура из объектов и массивов, синтаксис которой мы разбирали в первом уроке продолжения. ArduinoJson хранит их по-своему: число — в числовом виде, строку — указателем внутрь документа, булево — истина-ложь, null — пометкой «значения нет». Здесь разговор о другом: не о том, как читать поля — этому посвящён урок про чтение, — а о том, что становится чем, когда тип в пакете не тот, которого вы ждали. Живые API меняют представление полей без предупреждений, и типовой сюрприз дешевле встретить на столе, чем на работающей станции.
Полный список типов в JSON короток, и это облегчение: строка, число, true/false, null — и два структурных контейнера, объект и массив. В C++ типов больше, и мост между ними — приведение, которое библиотека делает при чтении и при печати. Сюрпризы рождаются именно на этом мосту: то, что сервер считал числом, на мосту оказывается строкой, то, что вы считали нулём, оказывается пометкой «значения нет». Ниже — пять сюжетов, каждый с условием, при котором он случается, и с лечением, которое я проверил на своей станции; разбор синтаксиса, зачем вообще кавычки и запятые, остался в уроке про синтаксис.
Четыре скаляра: что чем становится
Строка читается в const char* — и это указатель внутрь документа, о чём мы говорили в уроке про чтение: документ отчистили — строка повисла, нужна копия — String(city) или свой буфер. Число читается во float, int или long: правила приведения те же, что мы разбирали в уроке про типы переменных, только применяет их библиотека. Булево читается в bool: bool online = doc["online"]; из true даёт 1 — и это честная единица, Serial печатает её как 1, а не как слово.
Обратное направление капризнее, и на нём я обжигался при сборке исходящих пакетов: doc["flag"] = 1; записывает в документ число, и на выходе будет "flag":1, а не "flag":true. Чтобы получить булево, пишите булево: doc["ok"] = true;. Сервер, который строго сверяет тип поля, отвергнет единицу там, где ждал true, — и это будет тихий ответ 400 без объяснений. Правило у меня простое: форма значения в исходящем пакете должна повторять форму из документации API буквально, до последней кавычки.
null — самый коварный из четырёх. Пара "rain":null означает «значения отсутствует», и чтение даст ноль — но это подделка: осадки в ноль миллиметров и осадки «данных нет» — разные события, и журнал, где оба выглядят нулём, врёт. Отличать их учат isNull и containsKey из урока про чтение полей: ключ с null был — containsKey скажет 1, а пригодного значения нет — isNull скажет 1. Мы это сравнивали построчно; здесь важно одно: null в пакете — не ноль в переменной, и не надо давать журналу их смешивать.
Пустая строка и null — разные вещи, хотя на экране обе выглядят пустотой: "" — это строка без символов, у неё есть тип и место в документе; null — пометка «значения нет». Чтение пустой строки в const char* даст честную пустоту, чтение null — ноль; containsKey ответит 1 в обоих случаях. Практическое следствие: сервис, который шлёт пустую строку вместо отсутствия данных, обманывает проверку isNull — такие значения фильтруете длиной, а не наличием. Я однажды собирал статистику по городу, где пустые названия станций прибавляли «сухие дни» — до тех пор, пока не начал отбрасывать пустые строки отдельно.
Кавычка и точность: два дорогих сюрприза
Первый сюрприз уже погрыз мой экран: "21.4" — это строка из четырёх символов, а не число, и чтение во float даёт 0.00 молча. Серверы шлют числа строками по разным причинам — региональные настройки с запятой в дробях, CSV-наследие, страх перед округлением в JavaScript. Диагностика всегда одна: печатайте сырой пакет в Serial — кавычки видны глазами; на экране прибора они не живут, потому что туда доходит уже приведённое значение. Как вести такой журнал, чтобы он помогал, а не засорял, — в уроке про отладку по Serial.
Второй сюрприз — точность. float держит около семи значащих цифр: координата 55.751244 после кругосветки через документ возвращается примерно как 55.75124, и последний знак уже пляшет. Для погоды это неважно, для GPS-трекера — метры: пятый знак после запятой в координате стоит сантиметры, шестой — метры. Для координат я прошу сервер слать целые «миллиградусы» (55751244) — целые long переживают документ без потерь до своего потолка 2147483647. А вот таймштамп в миллисекундах за этот потолок уже выходит: 2147483648 в long не влезает, и счётчик перескакивает в минус — такие числа шлют строкой или разбивают на два поля.
На скетче для платы ниже — пакет, в котором каждый тип ведёт себя не так, как кажется на первый взгляд: строка вместо числа, честное булево, явный null и координата на грани точности. Симулятор воспроизводит большую часть истории, но не всё: где именно движок расходится с платой, я перечислил в подписи к sim-скетчу — там же, где расходится, в тексте сказано, что покажет настоящая плата.
Целое и дробное в JSON различаются точкой: 58 — целое, 21.4 — дробное, и чтение в «больший» тип проходит без потерь: int 58 во float станет 58.00. Обратное направление — с потерей: 21.4 в int обрежется до 21, и журнал этого не заметит. Правила те же, что в чистом C++, — библиотека не добавляет волшебства, она повторяет приведение. И ещё один случай, о котором стоит знать: число с ведущим нулём вроде 021 или шестнадцатеричное 0x1A в JSON числом не считается вовсе — разбор ответит InvalidInput, и виноват будет не сервер, а тот, кто собирал пакет руками.
#include <ArduinoJson.h> JsonDocument doc; // в v6: DynamicJsonDocument<768> void setup() { Serial.begin(9600); String packet = "{\"temp\":\"21.4\",\"humi\":58,\"online\":true,\"rain\":null,\"geo\":55.751244}"; Serial.println(packet); // сырой пакет в лог: кавычки видны DeserializationError err = deserializeJson(doc, packet); if (err) return; // дальше без документа не идём float t = doc["temp"]; // строка в число не превращается: Serial.print("t="); Serial.println(t); // 0.00 — молча, без предупреждений int h = doc["humi"]; // число и есть число: Serial.print("h="); Serial.println(h); // 58 bool on = doc["online"]; // true читается единицей: Serial.print("on="); Serial.println(on); // 1 Serial.print("est_rain="); Serial.println(doc.containsKey("rain")); // 1 — ключ в пакете был Serial.print("rain_null="); Serial.println(doc["rain"].isNull()); // 1 — пригодного значения нет Serial.print("geo="); // печатаем шесть знаков и сравниваем Serial.println(doc["geo"], 6); // с исходником: 55.75124... пятый знак уже врёт} void loop() {}- Кавычки по вкусу. Уберите кавычки вокруг 21.4 в пакете и запустите: значение станет числом, склейка с единицей заменится сложением — строка-текст превратится в арифметику.
- Булево в обе стороны. Поставьте "online":false и убедитесь, что печать вернёт 0; замените true строкой "true" — печатается 0: строка булевым не становится.
- Пропавший ключ. Вырежьте "tag":null целиком: строка est_tag сменится с 1 на 0 — null и отсутствие ключа различает containsKey.
- На настоящей плате. Перенесите скетч для платы и посмотрите на строку t=: ноль от строкового поля; добавьте atof(doc["temp"]) — и 21.4 вернётся уже в вашем скетче, без перепрошивки сервера.
| В пакете | Что прочитается | Что делать |
|---|---|---|
| "21.4" — число в кавычках | float 0.00 — молча | печатать сырой пакет; просить сервер слать числа или конвертировать atof |
| true / false | bool 1 / 0 | в исходящий пакет писать булево, не 1 и 0, если важна форма |
| null | 0, но isNull даст 1 | отличать «значения нет» от честного нуля проверкой |
| 55.751244 | около 55.75124 — семь значащих цифр | для координат слать целые миллиградусы: long переживает документ без потерь |
- Скаляров в JSON четыре: строка, число, true/false, null; кавычки превращают число в строку и обнуляют чтение
- "21.4" при чтении во float даёт 0.00 молча — кавычки видны только в печати сырого пакета
- true читается единицей; обратно doc["flag"] = 1 печатает 1, а не true — булево пишите булевым
- null означает «значения нет»: containsKey подтвердит ключ, isNull подтвердит пустоту — журнал не должен их смешивать
- float держит около семи значащих цифр: 55.751244 возвращается как 55.75124, пятый знак после запятой уже врёт
- Целые выше 2147483647 в long не влезают: таймштампы в миллисекундах шлют строкой или двумя полями
- 1. В пакете "temp":"21.4" — температура в кавычках. Что вернёт float t = doc["temp"]; на настоящей плате?
- 2. После doc["flag"] = 1; что напечатает serializeJson?
- 3. Пакет содержит "rain":null. Что вернёт doc.containsKey("rain")?
- 4. Сколько значащих цифр переживёт число в float при разборе на Uno?
- 5. Чем "rain":null отличается от отсутствующего ключа rain?
Типы — половина неожиданностей при разборе; вторая половина — поля, чей тип меняется от прошивки к прошивке. Для них в библиотеке есть явные проверки is и as: спросить «можно ли это прочитать как число» до того, как переменная получила ноль. Этому посвящён следующий урок. Домашнее задание с платой: возьмите живой API из урока про разбор ответа по HTTP, напечатайте сырой ответ в Serial и найдите глазами все четыре скалярных типа — включая null, если сервис экономит на данных. И проверьте обратное направление: соберите свой пакет с булевым полем и посмотрите, что напечатается после doc["ok"] = 1; — форма исходящих пакетов ломается тем же способом, что и входящих.
Частые вопросы по теме урока
- В пакете "temp":"21.4" — температура в кавычках. Что вернёт float t = doc["temp"]; на настоящей плате?
- Кавычки делают значение строкой; приведение строки к float в чтении не происходит, и переменная получает ноль — молча. Диагностика: печать сырого пакета. 0.00 — строка в число не приводится
- После doc["flag"] = 1; что напечатает serializeJson?
- Единица записывается как число и печатается без кавычек. Чтобы на выходе было слово true, в документ надо класть булево значение: doc["flag"] = true; "flag":1
- Пакет содержит "rain":null. Что вернёт doc.containsKey("rain")?
- containsKey отвечает за наличие ключа, и ключ с null считается присутствующим. Пригодность значения проверяет isNull — именно на этой паре различают null и отсутствие поля. 1 — ключ присутствовал в пакете
- Сколько значащих цифр переживёт число в float при разборе на Uno?
- float хранит около семи значащих цифр. Для координат это метры неточности: их шлют целыми миллиградусами, которые влезают в long без потерь. Около семи
- Чем "rain":null отличается от отсутствующего ключа rain?
- Чтение значения в обоих случаях даст ноль, но различить события помогает containsKey: для явного null ключ присутствует. Форму различия задаёт сам сервер. containsKey вернёт 1 для null и 0 для отсутствующего ключа