Метеосервис на даче отдавал город внутри вложенного объекта, а я полчаса стучался не в ту дверь. Чтение у меня было написано по образцу из первого знакомства с библиотекой: const char* city = doc["city"]; — и вместо «Omsk» на экране станции красовалась пустая строка. Сервер отвечал честно, разбор отрабатывал без единого замечания — просто город лежал на уровень глубже, чем я спрашивал.
Вскрытие заняло один взгляд на ответ в браузере: {"loc":{"city":"Omsk","reg":"Omskaya"},"now":{"temp":21.3,"hum":48}}. Ключ city лежал внутри объекта loc, температура — внутри now, а я искал их на верхнем уровне. Ещё полчаса я потратил на пересборку скетча, хотя правильный вопрос звучал иначе: не «как достать ключ», а «на каком он уровне» — это и есть тема урока.
Разберём, как устроена вложенность и зачем API группируют поля, как выглядит доступ цепочкой ключей на настоящей плате — doc["loc"]["city"] — и что молча вернёт чтение, когда промежуточного уровня нет. Потом повторим механику в симуляторе: там одно упрощение, о котором скажу заранее, — зато структуру чужого ответа можно рассмотреть построчно. Заодно договоримся о словах: документ — весь ответ целиком, карман — объект внутри, звено — один шаг цепочки; этими тремя словами я буду пользоваться до конца урока.
Матрёшка: уровень за уровнем
Вложенный объект — значение, которое само записано в фигурных скобках. В ответе выше верхний уровень держит три ключа: loc и now — объекты, ts — число. У каждого объекта свои пары «ключ: значение», и так можно уходить вглубь, сколько нужно: город внутри региона внутри страны — три уровня. Синтаксис мы разбирали в уроке про JSON-синтаксис, здесь переходим к практике доступа.
Группировка — не украшение, а контракт: у кармана своя роль. loc отвечает на «где», now — на «что сейчас», и сервис вправе дописать в loc часовой пояс и высоту над уровнем моря, не трогая now. У OpenWeatherMap температура живёт в main.temp, у других сервисов — в current: разбор по этим адресам отличается только цепочкой ключей, механика одна. Имя кармана — тоже данные: одни сервисы зовут погодный блок current, другие now, третьи main, и скетч, читающий адреса по карте структуры, переживает переименование правкой одной строки, а не переписыванием разбора.
Вложенность стоит байт: каждый уровень — свои записи внутри документа, и ответ с двумя вложенными объектами весит больше плоского с теми же полями. В симуляторе ниже такой ответ занимает 84 байта — замер печатает measureJson. На ESP32 с сотнями килобайт кучи это не тема для беспокойства, а на Uno с её двумя килобайтами уровни съедают четверть памяти, и я считаю каждый лишний карман. Ориентир из практики: короткий ключ дешёвый, длинная строка дорогая, а вложенный объект добавляет накладной расход уровня — тот же город, записанный плоско, занял бы байтов на пятнадцать меньше.
Цепочка ключей на настоящей плате
Доступ — цепочкой: doc["loc"]["city"] читается как «возьми loc, внутри него возьми city». На живой плате формы равнозначны: скобки и точки дают одно и то же — doc["loc"].city тоже сработает. В чужих скетчах и в моей практике чаще скобки: границы ключа видны глазом, а в редакторе без подсветки цепочка из точек сливается. Тип слева решает, как трактовать значение, — те же правила приведения, что в уроке про чтение полей и оператор |.
Грабля цепочки — молчание на любом звене. Промежуточного loc в ответе нет — doc["loc"] отдаёт null-прокси, и doc["loc"]["city"] продолжает молчать: ноль для числа, пустота для строки. Проверяйте звено перед спуском: doc["loc"].isNull() или containsKey на нужном уровне — у кармана loc свой containsKey("city"), который смотрит внутрь, а не на верх документа. Разница видна на переименовании: сервер сменил loc на location — containsKey("loc") вернёт ноль до всякого спуска, и скетч уйдёт в ветку «кармана нет» с печатью в лог, вместо молчаливой пустоты на экране. Сами коды провала разбора и их диагностика — в уроке про deserializeJson.
Приём для незнакомого ответа у меня один: сначала serializeJsonPretty(doc, Serial), потом цепочки. Печать с отступами рисует уровни — где город, где погода, что на верхнем уровне. Писать адреса по нарисованной карте быстрее, чем сверяться с документацией API, а заодно сразу видна цена вложенности в байтах; держать Serial-монитор открытым рядом с окном кода — привычка из первого знакомства с портом, и она окупается именно на таких распечатках.
Для длинных цепочек у меня есть приём со ступенькой: JsonVariant loc = doc["loc"]; const char* city = loc["city"]; — спуск двумя читаемыми шагами вместо одной строки из четырёх скобок. Проверка loc.isNull() между шагами честно скажет, на каком этаже оборвалась дорога, а адреса остальных полей кармана начинаются уже с loc — короче на одно звено каждый.
#include <ArduinoJson.h> JsonDocument doc; // v7; в v6 писали DynamicJsonDocument(768) void setup() { Serial.begin(9600); // Ответ метеосервиса: два вложенных объекта и время на верхнем уровне String resp = "{\"loc\":{\"city\":\"Omsk\",\"reg\":\"Omskaya\"},\"now\":{\"temp\":21.3,\"hum\":48},\"ts\":1717749600}"; DeserializationError err = deserializeJson(doc, resp); if (err) { Serial.println("razbor sorvan, dokument pust"); return; } // Цепочка: уровень за уровнем const char* city = doc["loc"]["city"]; const char* reg = doc["loc"]["reg"] | "?"; // нет поля — подставится запасное float t = doc["now"]["temp"]; int hum = doc["now"]["hum"]; Serial.print("Gorod: "); Serial.println(city); Serial.print("Region: "); Serial.println(reg); Serial.print("Temp: "); Serial.println(t); Serial.print("Vlazh: "); Serial.println(hum); // Проверка первого звена перед спуском: if (doc["loc"].isNull()) { Serial.println("bloka loc net, gorod ne chitat"); }} void loop() {}Вложенность в симуляторе
Разбор вложенного ответа в симуляторе настоящий: deserializeJson принимает строку и наполняет документ целиком, вместе с обоими объектами. Чтение с верхнего уровня показывается точечной формой: doc.ts печатает время без посредников. Скобочная форма чтения doc["temp"] в движке не эмулируется и вернёт ноль — на живой плате чаще пишут скобки, в сим-скетчах ниже точечная, механика доступа одна. Ёмкость под вложенный ответ выбирается как под плоский: DynamicJsonDocument(768) в скетчах ниже хватает с запасом — движок её не проверяет, а на плате ориентир прежний, в полтора-два раза больше ответа.
Цепочки по вложенным объектам движок не эмулирует: doc.loc.city вернёт ноль вместо «Omsk». Поэтому в скетче ниже город и регион разбираются вторым документом: тот же карман loc подаётся на вход отдельному deserializeJson, чтение — тем же точечным доступом. На плате это одна строка-цепочка, в симуляторе — два документа и честная пометка в коде. Проверки containsKey, печать структуры serializeJsonPretty и замер measureJson при этом работают по-настоящему.
Запустите скетч и сверьте три печати: vremya_sverhu покажет верхний уровень, est_loc и est_city_sverhu — разницу между «ключ есть» и «ключ есть на этом уровне», а структура с отступами — где что лежит. Эксперименты ниже меняют строку ответа: документ пересобирается сам, код чтения не трогается.
- Уровень глубже. Добавьте в loc ещё один карман: "geo":{"lat":54.9} — в печати структуры появится третий уровень. На плате адрес вырастет до doc["loc"]["geo"]["lat"] — цепочка тянется за ответом.
- Убрать карман. Вырежьте "loc":{...} целиком: est_loc сменит единицу на ноль, а блок чтения города при этом продолжит печатать Omsk из второго документа — на плате ровно этот случай ловит проверка промежуточного уровня.
- Сменить город. Замените Omsk на Tomsk в двух местах — в строке ответа и в пакете cityDoc: структура и чтение поменяются вместе, код не тронут.
- На настоящей плате. Перенесите разбор на Uno: вместо второго документа — цепочка const char* city = doc["loc"]["city"]; и проверка doc["loc"].isNull() перед спуском.
| Глубина | Адрес | Что вернёт при пропуске звена |
|---|---|---|
| верхний уровень | doc["ts"] | ноль или пустота; проверка containsKey("ts") |
| один уровень | doc["loc"]["city"] | молчаливая пустота: нет loc или нет city — снаружи не отличить |
| два уровня | doc["a"]["b"]["c"] | то же молчание; длинная цепочка — повод спуститься промежуточной переменной |
| объект и массив | doc["now"]["h24"][0] | комбинированный доступ: спуск и индекс в одной строке |
- Вложенный объект — значение в фигурных скобках внутри другого объекта; API группируют поля по ролям: loc — где, now — что сейчас
- Адрес значения — цепочка: doc["loc"]["city"]; скобки и точки на плате равнозначны, тип слева решает приведение
- Пропуск любого звена молчит: null-прокси даёт ноль или пустоту; проверяйте первое звено isNull или containsKey до спуска
- containsKey смотрит свой уровень: doc.containsKey("city") не найдёт город внутри loc — нужен doc["loc"].containsKey("city")
- serializeJsonPretty рисует уровни отступами: с карты структуры цепочки пишутся быстрее, чем по документации API
- В симуляторе разбор, containsKey, pretty-печать и measureJson настоящие; чтение — точечной формой, вложенный карман — вторым документом
- 1. Ключ city лежит внутри объекта loc. Как на плате выглядит чтение города?
- 2. Объекта loc в ответе нет вовсе. Что вернёт doc["loc"]["city"] на плате?
- 3. В документе {"loc":{"city":"Omsk"}} что ответит doc.containsKey("city")?
- 4. С чего разумно начинать знакомство с незнакомым вложенным ответом?
- 5. Почему в симуляторе вложенный карман разбирается вторым документом?
Город из матрёшки достаётся осознанно: уровень — часть адреса, проверка звена — до спуска, печать структуры — до написания цепочки. Следующая ступень раздела — ряды значений: сервер шлёт не один объект, а список, и список разбирается циклом с собственной граблёй — длиной, которую сервер меняет молча. А встроить вложенный разбор в реальный проект проще всего через сеть: в уроке про JSON по HTTP на ESP32 deserializeJson стоит сразу после приёма тела ответа — и вложенный пакет становится документом, дальше дело техники из этого урока. И не пренебрегайте отладкой: печать структуры serializeJsonPretty на любой незнакомый ответ — пять секунд работы, которые экономят вечер сравнения адресов.
Частые вопросы по теме урока
- Ключ city лежит внутри объекта loc. Как на плате выглядит чтение города?
- Адрес значения — путь по уровням: сначала карман loc, внутри — city. doc["city"] искал бы город на верхнем уровне и нашёл бы пустоту: уровень — половина адреса, без него чтение всегда мимо. doc["loc"]["city"] — цепочкой, уровень за уровнем
- Объекта loc в ответе нет вовсе. Что вернёт doc["loc"]["city"] на плате?
- Пропущенное звено даёт null-прокси, и чтение молча приводит его к нулю или пустоте. Проверку первого звена ставят до спуска: сорванный разбор из прошлого урока даёт ту же пустоту — снаружи не отличить. Пустоту для строки или ноль для числа — молча
- В документе {"loc":{"city":"Omsk"}} что ответит doc.containsKey("city")?
- containsKey проверяет ключи своего уровня. Город в документе есть, но его адрес — doc["loc"].containsKey("city"), а не верх документа; два уровня — две проверки, если от наличия поля зависит логика. Ноль: containsKey смотрит только на свой уровень, а city внутри loc
- С чего разумно начинать знакомство с незнакомым вложенным ответом?
- Печать с отступами рисует структуру: где карманы, где значения. Цепочки по нарисованной карте пишутся быстрее и без сверки с документацией, а заодно видна цена вложенности — байты на каждый уровень. С serializeJsonPretty в Serial — глазами увидеть уровни
- Почему в симуляторе вложенный карман разбирается вторым документом?
- Разбор полного вложенного ответа симулятор тянет, а доступ внутрь объекта — нет. Карман подаётся отдельным документом; на плате это одна цепочка, и раскладка по переменным при этом настоящая в обоих мирах. Цепочки по вложенным объектам движок не эмулирует; механика чтения при этом та же