JavaRush /Курсы /C++ SELF /JSON — объект, массив, примитивы

JSON — объект, массив, примитивы

C++ SELF
65 уровень , 0 лекция
Открыта

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
{ "key": value, ... }
Набор полей по именам. Ключ — строка.
array
[ value, value, ... ]
Упорядоченный список значений.
primitive
"text", 123, true, null
Лист дерева: строка, число, булево или 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} ] технически допустим, но почти всегда превращается в кашу: вы вынуждены писать много проверок типов, объяснять правила пользователю формата и постоянно бояться, что кто-то добавит новый «особый» случай. Для устойчивого формата массивы обычно делают однородными.

1
Задача
C++ SELF, 65 уровень, 0 лекция
Недоступна
Настройки приложения
Настройки приложения
1
Задача
C++ SELF, 65 уровень, 0 лекция
Недоступна
Профиль пользователя
Профиль пользователя
1
Задача
C++ SELF, 65 уровень, 0 лекция
Недоступна
Опознаватель примитивов
Опознаватель примитивов
1
Задача
C++ SELF, 65 уровень, 0 лекция
Недоступна
Дедлайн статусы
Дедлайн статусы
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ