1. Зачем нам JSON и как видеть структуру
Когда программа живёт дольше пяти минут, у неё появляется характерная привычка: ей нужно где-то хранить данные. Настройки, список задач, заметки, профиль пользователя — всё это хочется положить в файл так, чтобы потом можно было прочитать обратно. И вот тут начинается борьба форматов: «свой велосипед» против «нормального стандарта». JSON — один из самых популярных компромиссов: человекочитаемый, достаточно строгий, поддерживается почти везде.
Чтобы примеры были связаны, будем представлять, что у нас есть простое консольное приложение TaskPad: оно хранит список задач и немного настроек (например, имя пользователя и тему). Сегодня мы не будем программировать чтение JSON библиотекой — наша цель проще: научиться видеть структуру JSON глазами и понимать, где данные ломаются.
JSON как дерево значений
Очень полезно перестать думать о JSON как о «строке со скобками» и начать думать о нём как о дереве значений. В корне дерева лежит одно значение. Оно может быть объектом, массивом или примитивом. У объекта есть поля-ветки, у массива — элементы-ветки, у примитивов веток нет, это «листья». Это мышление спасает, когда вы отлаживаете «почему тут не то поле» или «почему парсер ругается на запятую».
Схематично это можно представить так:
flowchart TD
Root["Корень JSON-значения"]
Root --> Obj["object { ... }"]
Root --> Arr["array [ ... ]"]
Root --> Prim["primitive (string/number/bool/null)"]
Obj --> Key1["ключ: 'name'"]
Obj --> Key2["ключ: 'tasks'"]
Key2 --> Arr2["array задач"]
Arr2 --> TaskObj["object задачи"]
TaskObj --> Title["title: string"]
TaskObj --> Done["done: bool"]
Важно: в JSON всегда есть ровно одно корневое значение. Иногда это объект {...}, иногда массив [...]. Если вы видите «два объекта подряд» — это уже не JSON-документ, а два JSON-документа, склеенных без правил.
2. Семейства значений: object, array, primitives
Если собрать всё в одну картинку, JSON на самом деле очень маленький. В нём нет классов, дат, «интов», «лонгов», комментариев и прочих радостей. Есть всего три семейства узлов: объект, массив и примитивы. Но из них можно собрать почти любую структуру данных — примерно как из кубиков LEGO: деталей мало, а замки почему-то всё равно получаются огромные.
Ниже — «шпаргалка», которую полезно держать в голове:
| Семейство | Вид | Что означает |
|---|---|---|
| object | |
Набор полей по именам. Ключ — строка. |
| array | |
Упорядоченный список значений. |
| primitive | |
Лист дерева: строка, число, булево или null. |
Дальше разберём каждое семейство аккуратно, с примерами «на пальцах».
JSON object: фигурные скобки и поля по именам
JSON-объект — это то, что многие интуитивно воспринимают как «словарь» или «структуру»: у него есть поля, и каждое поле имеет имя. Имя поля в JSON — всегда строка в двойных кавычках. Значение может быть чем угодно: объектом, массивом, строкой, числом, true/false или null. Порядок полей в объекте не должен быть смыслом, даже если вам кажется, что «так красивее».
Представим настройки нашего TaskPad в JSON:
{
"user": "Alice",
"theme": "dark",
"autosave": true
}
А теперь маленький C++-пример: мы пока просто храним JSON как строку и печатаем, чтобы привыкнуть к виду и кавычкам. Заметьте, как удобно использовать raw-строку, чтобы не превращать код в фестиваль \" и \\.
#include <iostream>
#include <string>
int main() {
const std::string configText = R"({
"user": "Alice",
"theme": "dark",
"autosave": true
})";
std::cout << configText << '\n';
}
// (печатает JSON как есть)
Важная деталь, которую новички часто пропускают: ключи должны быть уникальными по смыслу, но сам JSON-формат не всегда гарантирует вам «как именно» обработаются дубли. Если написать так:
{ "user": "Alice", "user": "Bob" }
то это уже «данные со спорным смыслом»: некоторые парсеры возьмут последнее значение, некоторые — первое, некоторые скажут «ошибка». Поэтому дубли ключей — типичная причина «странных багов», когда вы уверены, что записали одно, а программа читает другое.
JSON array: квадратные скобки и порядок элементов
Массив в JSON — это список значений в квадратных скобках. В отличие от объекта, порядок в массиве важен. Если объект — это «по имени», то массив — это «по позиции». И тут появляется типичная привычка хорошего тона: стараться делать массивы однородными. То есть если это массив задач — пусть каждый элемент будет объектом задачи, а не «иногда строка, иногда число, иногда объект».
Список задач TaskPad может выглядеть так:
[
{ "title": "Buy milk", "done": false },
{ "title": "Read C++ book", "done": true }
]
И снова C++-кусочек — мы просто печатаем пример, чтобы глаз «запомнил» паттерн: квадратные скобки, запятые между элементами, объекты внутри массива.
#include <iostream>
#include <string>
int main() {
const std::string tasksText = R"([
{ "title": "Buy milk", "done": false },
{ "title": "Read C++ book", "done": true }
])";
std::cout << tasksText << '\n';
}
// (печатает JSON-массив)
Когда массив делают разнотипным (например, [1, "two", {"three": 3}]), это почти всегда признак либо «быстро накидали прототип», либо «у формата нет контракта». В учебных и прикладных проектах лучше избегать такого: потом вы сами же будете страдать, пытаясь объяснить, почему третий элемент — объект, а не строка, и кто вообще так придумал (спойлер: это были вы неделю назад).
JSON primitives: четыре «атома» данных
Примитивы в JSON — это листья дерева. Они не содержат вложенных структур. Их всего четыре вида: строка, число, булево значение и null. И вот здесь начинается самое весёлое: в C++ типы строгие, а JSON «число» — просто число. Поэтому важно заранее (как дизайнер формата) решить, что вы ожидаете: целое, вещественное, ограниченный диапазон, допустим ли null и так далее.
Пример «смешанного» объекта задачи, где встречаются все примитивы:
{
"title": "Write lecture",
"done": false,
"priority": 2,
"note": null
}
Запомним главное: в JSON нет типа «char», нет «date», нет «int64». Есть только «number». Всё остальное — договорённость вашего формата и вашей программы.
3. Строки и экранирование: где ломаются кавычки
Самая частая проблема с JSON у новичков — это строки. Потому что строка в JSON обязана быть в двойных кавычках, а внутри строки кавычки и специальные символы нужно экранировать. И это место, где люди внезапно узнают, что кавычки бывают не только в жизни («он сказал…»), но и в данных, и они тоже хотят жить.
Пример корректной JSON-строки:
{ "title": "He said: \"Hello\"" }
То есть внутри строки двойная кавычка превращается в \". Если вы забудете обратный слеш, JSON «закончит строку раньше времени» и дальше будет ругаться так, будто вы сломали ему судьбу.
Ещё один типичный момент — переносы строк. В JSON внутри строки нельзя просто нажать Enter и считать, что «ну это же текст». Перенос внутри строки должен быть записан как \n. Поэтому многострочные тексты часто либо хранят с \n, либо выносят в массив строк.
И маленький C++-пример: в C++ мы тоже живём в мире строк и экранирования. Raw-строки помогают не экранировать JSON для C++ (иначе получилось бы «экранирование для экранирования», и вы бы начали подозревать, что реальность — это симуляция).
#include <iostream>
#include <string>
int main() {
const std::string text = R"({"title":"He said: \"Hello\""})";
std::cout << text << '\n'; // {"title":"He said: \"Hello\""}
}
Обратите внимание на забавный факт: внутри raw-строки R"( ... )" вам не нужно экранировать кавычки для C++, но внутри JSON они всё равно экранируются по правилам JSON. То есть raw-строка не «отменяет» правила JSON, она просто не мешает вам эти правила записать.
4. Числа, bool и null: «"42"», 42 и 42.0 — это разные звери
На уровне «смотрю глазами» числа и булевы значения в JSON кажутся простыми. Но как только вы начинаете читать эти данные в программу, выясняется, что простота была временной. JSON не различает int и double как язык C++: там просто number. А в вашей модели данных, скорее всего, хочется строгости: возраст — целое, рейтинг — дробное, флажок — булево, отсутствие значения — null или отсутствие поля.
Самая популярная ловушка: число как строка.
{ "age": "42" }
и
{ "age": 42 }
Визуально похоже, а смысл принципиально разный. В первом случае это текст "42", во втором — число 42. Если вы потом будете делать проверки или сортировки, разница внезапно станет очень заметной: строка "100" «меньше» строки "42" по лексикографическому порядку, потому что '1' < '4'. И вот вы уже сортируете людей по возрасту и получаете магию.
Булевы значения в JSON — строго true и false в нижнем регистре. Варианты True, FALSE, yes, 0/1 — это уже «не JSON» или «не по контракту».
null — это отдельный примитив. Он не в кавычках. Это не строка "null" и не «пустая строка». Это именно значение «ничего».
{ "note": null }
Как это выглядит в нашем «текстовом» C++-мире на сегодня:
#include <iostream>
#include <string>
int main() {
const std::string example1 = R"({"age": 42})";
const std::string example2 = R"({"age": "42"})";
const std::string example3 = R"({"note": null, "done": true})";
std::cout << example1 << '\n'; // {"age": 42}
std::cout << example2 << '\n'; // {"age": "42"}
std::cout << example3 << '\n'; // {"note": null, "done": true}
}
Пока мы это не парсим, но мы уже тренируем важный навык: видеть тип по синтаксису. Кавычки — строка. Нет кавычек — число/true/false/null (или ошибка).
5. «Поля нет» и «поле = null»: две разные ситуации
Это одна из самых дорогих по багам тем в любом формате данных: отличать «значение отсутствует» от «значение явно задано как пустое». В JSON эти состояния различаются очень чётко: если поля нет — его нет. Если поле есть и равно null — оно есть, и это отдельный смысловой сигнал.
Сравните две задачи.
Вариант А — поле отсутствует:
{ "title": "Buy milk", "done": false }
Вариант Б — поле есть, но оно null:
{ "title": "Buy milk", "done": false, "deadline": null }
С точки зрения «контракта формата» это могут быть разные смыслы. Например, отсутствие deadline может означать «мы ещё не придумали дедлайн», а deadline: null — «дедлайна принципиально нет и не будет». Или наоборот. Главное — вы должны заранее договориться, что это значит, иначе чтение данных превратится в гадание на кофейной гуще.
Есть и третья ловушка: поле есть, но оно пустая строка.
{ "deadline": "" }
Это не то же самое, что null, и тем более не то же самое, что «поля нет». Иногда пустая строка — допустимое значение, иногда это ошибка данных. Но если вы не договорились, то потом в коде появятся «магические» проверки if (s == ""), и жизнь станет чуть менее радостной.
Небольшой C++-пример, который показывает, что «поля нет» и «поле = null» — это разные тексты, даже до всякого парсинга:
#include <iostream>
#include <string>
int main() {
const std::string noField = R"({"title":"Buy milk"})";
const std::string nullField = R"({"title":"Buy milk","deadline":null})";
std::cout << noField << '\n'; // {"title":"Buy milk"}
std::cout << nullField << '\n'; // {"title":"Buy milk","deadline":null}
}
Сегодня наша цель — запомнить: отсутствие поля и null — не одно и то же. В следующих лекциях это станет центральным моментом при чтении и валидации данных.
6. Типовые ошибки JSON-данных и как их распознавать глазами
JSON строгий. Он не злой — он просто не терпит «ну вы поняли, что я имел в виду». Поэтому большинство ошибок можно поймать, просто внимательно посмотрев на текст. И это хороший навык: чем раньше вы научитесь «дебажить глазами», тем меньше времени потратите на бесконечное «почему парсер орёт».
Одна из самых частых ошибок — лишняя запятая в конце списка полей или элементов массива. В некоторых языках (и в некоторых конфигурационных форматах) trailing comma разрешён, но в стандартном JSON — нет.
Плохой пример:
{ "user": "Alice", }
То же самое в массиве:
[ 1, 2, 3, ]
Следующая классика — неправильные кавычки. JSON признаёт только двойные кавычки "...". Одинарные кавычки '...' — это не JSON, даже если некоторые люди очень стараются сделать вид, что это «почти то же самое».
Плохой пример:
{ 'user': 'Alice' }
Ещё одна распространённая история — попытка добавить комментарии. JSON комментарии не поддерживает. Никаких // и /* ... */. Если вам нужен формат с комментариями, это уже другой формат (например, JSON5), и парсер стандартного JSON будет прав, отказав вам.
Плохой пример:
{
"user": "Alice" // пользователь
}
Дальше идёт «псевдо-JSON» из других миров: True/False вместо true/false, NULL вместо null, или даже NaN и Infinity. В некоторых системах это встречается, но это уже не JSON. Если вы это видите, значит, у вас данные приехали не из JSON-мира, а из «мира, где все договорились как попало».
Плохие примеры:
{ "done": True }
{ "value": NaN }
И отдельная боль — незакрытые скобки и кавычки. Тут помогает простая привычка: если документ не маленький, ищите структуру «снаружи внутрь». Сначала убеждаемся, что у корня есть пара {...} или [...]. Потом смотрим, что внутри всё симметрично, и только потом уже вчитываемся в поля.
Для тренировки «чтения структуры» полезно смотреть на мини-JSON нашего TaskPad (настройки + задачи) и задавать себе вопросы: корень — объект или массив? где массив задач? где примитивы? какие поля обязательны?
#include <iostream>
#include <string>
int main() {
const std::string saveText = R"({
"user": "Alice",
"tasks": [
{ "title": "Buy milk", "done": false },
{ "title": "Read C++ book", "done": true }
]
})";
std::cout << saveText << '\n';
}
Да, это пока просто текст. Но уже сейчас вы должны узнавать паттерн: корень-объект, у него поле tasks, значение этого поля — массив, элементы массива — объекты задач.
7. Типичные ошибки
Ошибка №1: путать JSON с «почти JSON» (комментарии, лишние запятые, одинарные кавычки).
Это выглядит безобидно, потому что человеку понятно, что имелось в виду. Но парсер работает не «по смыслу», а по строгим правилам. В итоге вы получаете ошибку «где-то тут что-то не так», хотя по ощущениям «всё нормально». Лечится это дисциплиной: если формат называется JSON, значит, никаких расширений без явного решения.
Ошибка №2: хранить числа строками («"42"» вместо 42) и потом ждать корректной математики.
На этапе записи данных часто кажется удобным «всё хранить строками, так проще». А потом внезапно нужно сравнить, отсортировать, посчитать, и оказывается, что у вас не данные, а коллекция текстов. Особенно заметно это проявляется в сортировках, где "100" оказывается «меньше» "42". Для чисел выбирайте number, а строки оставляйте строкам.
Ошибка №3: опираться на порядок ключей в object.
Иногда люди начинают воспринимать JSON-объект как «структуру с фиксированным порядком полей», потому что в файле красиво: сначала id, потом name, потом tasks. Но по смыслу JSON object — это набор полей по именам, и порядок не должен иметь значения. Если ваш код или ваши ожидания завязаны на порядок, формат начинает «скрипеть» при первом же сохранении другим инструментом или другой библиотекой.
Ошибка №4: не различать «поля нет» и «поле равно null», а потом спорить с собственным форматом.
Эти два состояния — разные, и JSON даёт вам оба. Но если вы заранее не описали контракт (что означает отсутствие, что означает null), то через некоторое время в проекте появятся противоречивые файлы: где-то поле не пишут, где-то пишут null, где-то пишут пустую строку. Потом это приходится «чинить миграциями», и это уже не весело. Лучше договориться сразу.
Ошибка №5: делать массивы разнотипными без крайней необходимости.
Массив [ {задача}, {задача}, {задача} ] читается, валидируется и расширяется предсказуемо. Массив [ "Buy milk", 7, true, null, {"done":false} ] технически допустим, но почти всегда превращается в кашу: вы вынуждены писать много проверок типов, объяснять правила пользователю формата и постоянно бояться, что кто-то добавит новый «особый» случай. Для устойчивого формата массивы обычно делают однородными.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ