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

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

Swift SELF
58 уровень , 0 лекция
Открыта

1. Зачем нам JSON

Если вы когда‑нибудь сохраняли заметку в приложении, отправляли данные на сервер или просто видели ответ от API, вы уже встречали JSON — даже если не знали, что это он. JSON можно воспринимать как очень популярный формат «упакованного текста», который одинаково неплохо читают и люди, и программы. Он не самый быстрый, не самый компактный, но зато обычно понятный и переносимый.

Проблема начинается там, где новичок думает: «Ну это же просто текст, сейчас распарсим как‑нибудь». А потом внезапно выясняется, что один и тот же ключ может быть числом, строкой или вообще отсутствовать — и ваша программа превращается в детектив: «а где же поле id, и почему оно сегодня "7", а не 7?». Именно поэтому мы будем говорить не только про «синтаксис JSON», но и про схему: договорённость о том, что именно лежит внутри.

Чтобы связать это с нашим учебным приложением, представим, что мы делаем консольную мини‑библиотеку (условный LibraryCLI): добавляем книги, ищем их, а однажды захотим их сохранить. Для хранения нам нужен формат — и JSON как раз один из самых типичных вариантов.

2. Структура JSON: контейнеры и примитивы

JSON держится на очень простой идее: есть контейнеры (составные типы), которые могут содержать другие значения, и есть примитивы (атомарные значения). Это как коробки и предметы. Коробка может содержать коробки и предметы, а предмет внутри себя уже ничего не хранит.

В JSON всего два контейнера: object и array. И всего четыре «простых» типа значений: string, number, bool, null. Если вы это запомните — вы уже на полпути к спокойной жизни.

Для наглядности — маленькая таблица:

Категория Вид в JSON Пример
Object (объект)
{ ... }
{ "title": "Dune" }
Array (массив)
[ ... ]
[1, 2, 3]
String (строка)
"..."
"Swift"
Number (число)
123, 3.14
2024
Bool (логическое)
true / false
false
Null
null
null

Важно: JSON — формат текстовый, но по смыслу он описывает структуру данных. Как раз на этом месте и появляется трение между «слабо типизированным» JSON и «строго типизированным» Swift: JSON легко позволяет «что угодно», а Swift хочет точных типов. Эту фундаментальную проблему в мире Swift обсуждают давно: когда данные «внешние», они не обязаны совпадать с нашими ожиданиями.

3. JSON‑объект {}: ключ → значение

JSON‑объект — это набор пар «ключ → значение». В быту удобно думать о нём как о Dictionary, только с жёсткими правилами: ключи всегда строки и всегда пишутся в двойных кавычках. То есть {"id": 1} — да, {id: 1} — нет (это уже «почти‑JSON», который любит JavaScript, но JSON такой трюк не одобряет).

С практической точки зрения объект хорош, когда у сущности есть именованные поля: id, title, author. Именно в таком виде мы бы описали книгу в нашем приложении.

Пример (просто строка, без парсинга):

let bookJSON = """
{
  "id": 1,
  "title": "Dune",
  "isAvailable": true
}
"""

print(bookJSON.count) // например, 55 (зависит от пробелов/переводов строк)

Обратите внимание на детали: ключи в кавычках, значения разных типов, и JSON «не против», что id — число, title — строка, isAvailable — логическое. Это нормально и удобно.

Важный нюанс проектирования: если вы выбрали ключ "isAvailable", то это часть контракта формата. Переименовать в "available" «просто потому что так красивее» — значит сломать совместимость чтения старых данных.

4. JSON‑массив []: список значений

Массив в JSON — это последовательность значений, разделённых запятыми. Значения могут быть любыми: примитивами, объектами, массивами. Если объект — это «карточка с полями», то массив — это «список карточек».

Для нашей мини‑библиотеки массив — естественная форма для списка книг: [book1, book2, book3].

let tagsJSON = #"""
["swift", "json", "cli"]
"""#

print(tagsJSON) // ["swift", "json", "cli"]

Почему тут #""" ... """#? Это raw‑строка: внутри можно писать двойные кавычки без экранирования. Мы не обязаны это использовать, но иногда так проще читать глазами.

Ещё один пример — массив объектов:

let booksJSON = """
[
  { "id": 1, "title": "Dune" },
  { "id": 2, "title": "1984" }
]
"""

print(booksJSON.contains("\"title\"")) // true

Главная практическая мысль: если корень JSON — массив, то на верхнем уровне у вас нет места для метаданных. Это станет важным, когда мы захотим хранить версию схемы, дату обновления, источник данных и т.д. (пока просто запомним эту мысль).

5. Примитивы JSON: string / number / bool / null

Примитивы кажутся простыми, пока вы не начинаете их хранить «всерьёз». Именно на примитивах чаще всего и ломаются ожидания: "7" (строка) внезапно приезжает вместо 7 (числа), null появляется там, где вы ждали строку, а true превращается в "true" (строку) — и всё, приехали.

В JSON примитивы такие:

  • string: только в двойных кавычках, с экранированием специальных символов (перевод строки, кавычки).
  • number: в JSON нет отдельного Int и Double, это просто «число». Внутри реализации оно может стать целым или дробным, но в тексте это одна категория.
  • bool: только true или false маленькими буквами.
  • null: специальное значение «пусто».
let primitivesJSON = """
{
  "title": "Dune",
  "year": 1965,
  "rating": 4.8,
  "isClassic": true,
  "subtitle": null
}
"""

print(primitivesJSON.contains("null")) // true

С точки зрения схемы (договорённости) важно решить: что означает null? Это «значение неизвестно», «значение намеренно пустое», «значение удалено»? JSON позволяет написать null, но смысл задаёте вы.

6. Вложенность: JSON как дерево данных

Почти любой реальный JSON — вложенный. Внутри объекта лежит массив, внутри массива лежат объекты, внутри них снова массивы… и так далее. Это выглядит как дерево, где каждая ветка — это либо массив, либо объект, а листья — примитивы.

Для книги это может быть: объект книги содержит массив авторов (строки), массив тегов, а ещё вложенный объект meta (например, «добавлено в избранное»).

let nestedJSON = """
{
  "id": 1,
  "title": "Dune",
  "authors": ["Frank Herbert"],
  "meta": { "addedBy": "user", "isFavorite": false }
}
"""

print(nestedJSON.contains("\"meta\"")) // true

Если вам пока тяжело «держать в голове» такую структуру, помогает простая мысль: вложенность — это просто значения внутри значений. И да, это абсолютно нормальное состояние для JSON.

Можно даже нарисовать «форму» данных:

flowchart TD
    A[Book JSON Object] --> B["id: number"]
    A --> C["title: string"]
    A --> D["authors: array"]
    D --> E["author: string"]
    A --> F["meta: object"]
    F --> G["addedBy: string"]
    F --> H["isFavorite: bool"]

7. Схема и дизайн хранения для LibraryCLI

Что такое схема и почему без неё больно

Вот здесь начинается взрослая жизнь. Сама по себе строка JSON не гарантирует вам почти ничего, кроме того, что это валидный JSON. Но для программы важнее другое: совпадает ли структура данных с тем, что мы ожидаем.

Схема (в нашем учебном смысле) — это договорённость о четырёх вещах:

1) какой тип у корня (объект {} или массив []),
2) какие ключи у объектов и как они называются,
3) какие типы у значений (строка/число/массив/объект…),
4) какие поля обязательны, а какие могут отсутствовать.

Почему это важно? Потому что Swift строгий: если вы написали, что у книги есть id: Int, то вы внутренне пообещали себе, что из данных всегда можно получить целое число. А JSON вам ничего такого не обещал. Именно это расхождение — ключевой источник ошибок и головной боли при работе с реальными данными.

Небольшая аналогия: JSON без схемы — это «посылка без описи вложения». Вроде коробка приехала, но что внутри — узнаете только вскрыв. А когда вскрыли — может оказаться, что там не книга, а керамическая кружка, и вы уже потрясли коробку по пути (в коде это называется fatalError, но мы сегодня добрые).

«Нет значения»: ключ отсутствует vs ключ есть, но null

На практике это один из самых тонких моментов, который часто ломает логику — даже у людей, которые уже «умеют JSON». В JSON есть два разных сценария «пустоты», и они могут означать разное:

1) ключ отсутствует вообще
2) ключ есть, но значение null

Сравните:

let missingKeyJSON = """
{ "id": 1, "title": "Dune" }
"""

let nullKeyJSON = """
{ "id": 1, "title": "Dune", "subtitle": null }
"""

print(missingKeyJSON.contains("subtitle")) // false
print(nullKeyJSON.contains("subtitle"))    // true

И вот вопрос схемы: что для вашего приложения означает отсутствие subtitle? А что означает subtitle: null? Это одно и то же или разные состояния?

В формате хранения для нашей библиотеки обычно удобно считать, что subtitle может быть опциональным: иногда его нет — и всё нормально. Но даже в этом случае лучше выбрать один стиль: либо поле может отсутствовать, либо оно всегда есть, но иногда null. Оба подхода жизнеспособны — важно, чтобы вы не перемешивали их случайно.

Как будет выглядеть файл целиком: корень и метаданные

Сейчас будет очень практичный момент: мы ещё не пишем Codable‑модели и не кодируем/декодируем, но уже решаем, как будет выглядеть файл. Это решение — как выбор планировки квартиры: потом можно передвинуть стул, но снести несущую стену сложнее.

Представим, что мы хотим хранить список книг. У книги пусть будут такие поля (как идея): id, title, опционально subtitle, массив authors, массив tags.

Как может выглядеть один Book в JSON? Пример:

let bookV1JSON = """
{
  "id": 1,
  "title": "Dune",
  "subtitle": null,
  "authors": ["Frank Herbert"],
  "tags": ["sci-fi", "classic"]
}
"""

print(bookV1JSON.contains("\"authors\"")) // true

Теперь ключевой выбор схемы: что будет корнем файла?

Если корень — массив книг, то это просто и компактно:

[
  { ...book1... },
  { ...book2... }
]

Но как только вы захотите хранить метаданные (например, версию схемы), массив вас ограничит: метаданные положить некуда. Поэтому в инженерной практике часто делают корнем объект‑контейнер, где есть мета‑поля и массив данных.

Очень типичное поле — "schemaVersion": оно встречается в реальных JSON‑форматах как механизм «версионирования схемы». Например, в форматах вокруг SwiftPM тоже используется поле "schemaVersion" в JSON‑манифестах, чтобы различать версии структуры файла.

Мы здесь пока не фиксируем окончательный формат (это будет отдельная лекция дня), но важная мысль уже сейчас: схема — это не только “как выглядит Book”, но и “как выглядит файл целиком”.

8. Быстрые проверки и валидность JSON

Мини‑проверка формы JSON «на глаз»

Иногда полезно уметь быстро посмотреть на JSON как на текст и понять «корень — объект или массив», не делая настоящего парсинга. Это не замена нормальному декодированию, но как диагностика — работает.

func jsonRootKind(_ text: String) -> String {
    let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
    if trimmed.hasPrefix("{") { return "object" }
    if trimmed.hasPrefix("[") { return "array" }
    return "unknown"
}

print(jsonRootKind("{\"id\":1}")) // object
print(jsonRootKind("[1,2,3]"))    // array

Это намеренно «тупая» проверка, но она помогает новичку перестать путаться: {} и [] — это два разных «контейнера», и от этого зависит, как вы будете интерпретировать данные.

Валидный JSON vs «почти JSON»

JSON строгий и часто не прощает «чуть‑чуть не так». Особенно больно тем, кто привык к языкам/форматам, где можно комментарии, одинарные кавычки и «запятая на удачу».

Вот типичный «почти JSON», который выглядит правдоподобно, но является невалидным JSON:

Пример (как строка — компилируется, но это не валидный JSON):

let almostJSON = """
{
  'id': 1, // одинарные кавычки и комментарии
  "title": "Dune",
}
"""

print(almostJSON) // выглядит похоже, но это не JSON

Здесь сразу три проблемы: одинарные кавычки, комментарии, запятая после последнего элемента. И это не придирки: если вы храните данные в файле, любая такая «мелочь» превращает чтение в ошибку.

9. Типичные ошибки

Ошибка №1: путать JSON и «объект в JavaScript».
Очень частая история: человек пишет { id: 1 } и искренне считает, что это JSON. Это похоже на JSON, но JSON требует, чтобы ключи были строками в двойных кавычках: { "id": 1 }. Если держать в голове правило «в JSON ключ — всегда строка», вы перестаёте наступать на эти грабли.

Ошибка №2: не договориться о корне и постоянно “переобуваться”.
Сегодня вы сохранили файл как массив книг, завтра решили, что нужен объект с метаданными, а послезавтра добавили ещё один массив рядом. В результате старые данные читать сложно, новые — ещё сложнее. Корень файла — часть схемы: выбрали — держимся.

Ошибка №3: смешивать типы “по настроению”: id то число, то строка.
JSON позволяет хранить "id": "7" и "id": 7, но для приложения это две разные реальности. Если вы хотите числовой идентификатор — храните числом. Если хотите строковый — храните строкой. Смешивание почти всегда приводит к тому, что половину времени вы «чините данные», а не развиваете программу.

Ошибка №4: путать отсутствие ключа и null.
В одном месте вы не записали subtitle, в другом записали subtitle: null, в третьем записали subtitle: "". А потом пытаетесь понять, почему в интерфейсе где-то показывается «(нет подзаголовка)», а где-то пустая строка, а где-то вообще падает логика. Эти случаи могут быть разными состояниями — но тогда это должно быть частью схемы. Если вам не нужна такая тонкость, выберите один вариант и придерживайтесь его.

Ошибка №5: менять имена ключей “ради красоты”.
Сегодня у вас "schemaVersion", завтра вы решили, что "schema" короче, а послезавтра — что лучше "version". Такие изменения ломают совместимость и превращают простое чтение JSON в миграции и костыли. В реальных инструментах, которые хранят JSON‑файлы, ключи и "schemaVersion" обычно фиксируются как часть контракта формата.

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