# JSON: чтение, запись и сохранение данных<no value>

## Что мы изучим?

- Что такое **JSON** и как он выглядит.
- Как JSON соответствует **dict** и **list** в Python.
- **Сохранение** в файл и загрузку из него.
- **Преобразование** между строкой JSON и данными.

## Зачем это нужно?

В предыдущем уроке мы сохраняли текст и для этого придумали собственный «формат»: `Dana,90` в каждой строке. А теперь допустим, что у каждого студента есть **список** оценок, **адрес** с городом и улицей и **флаг** `is_active`. Как сохранить это в строке текста? Каждый придумает свой формат, и придётся писать сложный код разбора.

**JSON** — **стандартный** формат для структурированных данных. Его умеет читать и писать любой язык программирования. И главное:

- **Тема ООП** — проект будет сохраняться в JSON.
- **Темы бэкенда и баз данных** — **каждый** запрос и ответ в API отправляется в формате JSON. FastAPI автоматически превращает dict Python в JSON и обратно.

## Объяснение с основ

### Что такое JSON?

**JSON** = **J**ava**S**cript **O**bject **N**otation. Он родился в JavaScript, но сегодня это общий формат. Файл JSON — **текстовый файл** с определённой структурой, который выглядит почти так же, как dict и list в Python:

```json
{
  "name": "Dana",
  "age": 24,
  "is_active": true,
  "grades": [90, 85, 100],
  "address": {"city": "Haifa", "street": "Herzl 10"},
  "phone": null
}
```

### Соответствие между JSON и Python

| JSON | Python |
|---|---|
| object `{ }` | `dict` |
| array `[ ]` | `list` |
| string `"text"` | `str` |
| number `42`, `3.5` | `int`, `float` |
| `true` / `false` | `True` / `False` |
| `null` | `None` |

### Важные правила JSON (отличаются от Python!)

- Строки и ключи — **только в двойных кавычках** `"..."`. Одинарные кавычки `'...'` недопустимы.
- `true`, `false`, `null` — **строчными** буквами.
- **Без запятой** после последнего элемента.
- **Без комментариев**.
- Ключи объекта — **всегда строки**.

### Сериализация и десериализация

| Термин | Направление | Функция |
|---|---|---|
| **Сериализация** (Serialization) | Данные Python → текст JSON | `json.dump` / `json.dumps` |
| **Десериализация** (Deserialization) | Текст JSON → данные Python | `json.load` / `json.loads` |

**Как запомнить:** **s** в конце (`dumps`, `loads`) = **s**tring, работа со **строкой**. Без s (`dump`, `load`) = работа с **файлом**.

{{< box "analogy" "Аналогия: упаковка мебели при переезде" >}}
Диван в гостиной (dict в памяти) не пройдёт в дверь как есть. **Сериализация** = разобрать его и упаковать в плоскую коробку с инструкцией по сборке (текст JSON), которую можно хранить на складе (в файле) или отправить грузовиком (по сети). **Десериализация** = собрать его снова в новом доме и получить точно такой же диван.
{{< /box >}}

## Синтаксис

```python
import json

data = {"name": "Dana", "grades": [90, 85]}

# file
with open(path, "w", encoding="utf-8") as file:
    json.dump(data, file, indent=2, ensure_ascii=False)   # save

with open(path, "r", encoding="utf-8") as file:
    data = json.load(file)                                # load

# string
text = json.dumps(data)        # dict -> JSON string
data = json.loads(text)        # JSON string -> dict
```

- `indent=2` — записывает «красивый» JSON с переводами строк и отступами. Без него всё в одной строке.
- `ensure_ascii=False` — неанглийский текст (например, русский или иврит) сохраняется как есть, а не в виде escape-кодов (обратная косая черта, `u` и четыре шестнадцатеричные цифры на каждую букву).

## Первый пример кода

`week-2/files/save_profile.py`:

```python
import json
from pathlib import Path

PROFILE_FILE = Path(__file__).parent / "profile.json"

profile = {
    "name": "Dana",
    "age": 24,
    "is_active": True,
    "grades": [90, 85, 100],
    "address": {"city": "Haifa", "street": "Herzl 10"},
    "phone": None,
}

with open(PROFILE_FILE, "w", encoding="utf-8") as file:
    json.dump(profile, file, indent=2, ensure_ascii=False)

with open(PROFILE_FILE, "r", encoding="utf-8") as file:
    loaded = json.load(file)

print("Loaded type:", type(loaded))
print("City:", loaded["address"]["city"])
print("Average:", sum(loaded["grades"]) / len(loaded["grades"]))
print("Same as original?", loaded == profile)
```

**Вывод:**

```text
Loaded type: <class 'dict'>
City: Haifa
Average: 91.66666666666667
Same as original? True
```

**Созданный `profile.json`:**

```json
{
  "name": "Dana",
  "age": 24,
  "is_active": true,
  "grades": [
    90,
    85,
    100
  ],
  "address": {
    "city": "Haifa",
    "street": "Herzl 10"
  },
  "phone": null
}
```

## Разбор кода построчно

| Строка | Объяснение |
|---|---|
| `import json` | Стандартный модуль. Установка не нужна. |
| `json.dump(profile, file, indent=2, ...)` | **Сериализация**: превращает dict в текст JSON и записывает его в файл. `True` стало `true`, а `None` — `null`. |
| `json.load(file)` | **Десериализация**: читает текст и строит из него новый dict. |
| `type(loaded)` | Совершенно обычный `dict`. С ним можно работать, как с любым словарём. |
| `loaded["address"]["city"]` | Доступ к вложенной структуре, как в теме структур данных и функций. |
| `loaded == profile` | `True`: данные полностью пережили «путешествие» в файл и обратно. (Но `loaded is profile` — `False`, потому что это новый объект.) |

## Ещё один пример

**Маленькая «база данных» в файле JSON** — загрузка со значением по умолчанию, добавление, сохранение и обработка испорченного файла. Именно этот шаблон будет использоваться в проекте ООП. `week-2/files/contacts_db.py`:

```python
import json
import logging
from pathlib import Path

logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")

DB_FILE = Path(__file__).parent / "contacts.json"

def load_contacts():
    if not DB_FILE.exists():
        logging.info("No database file yet - starting empty")
        return []
    try:
        with open(DB_FILE, "r", encoding="utf-8") as file:
            return json.load(file)
    except json.JSONDecodeError:
        logging.error("Database file is corrupted - starting empty")
        return []

def save_contacts(contacts):
    with open(DB_FILE, "w", encoding="utf-8") as file:
        json.dump(contacts, file, indent=2, ensure_ascii=False)
    logging.info("Saved %s contacts", len(contacts))

def add_contact(contacts, name, phone):
    contact = {"id": len(contacts) + 1, "name": name, "phone": phone}
    contacts.append(contact)
    return contact

def main():
    contacts = load_contacts()
    new_contact = add_contact(contacts, "Dana", "050-1234567")
    print("Added:", new_contact)
    save_contacts(contacts)
    print("Total contacts in file:", len(contacts))

if __name__ == "__main__":
    main()
```

**Первый запуск:**

```text
INFO No database file yet - starting empty
Added: {'id': 1, 'name': 'Dana', 'phone': '050-1234567'}
INFO Saved 1 contacts
Total contacts in file: 1
```

**Второй запуск:**

```text
Added: {'id': 2, 'name': 'Dana', 'phone': '050-1234567'}
INFO Saved 2 contacts
Total contacts in file: 2
```

**Разбор:**

- **Файл хранит список словарей**: ровно «таблицу» записей.
- `load_contacts` обрабатывает два случая: **файла нет** (первый запуск) → пустой список. **Файл испорчен** (`json.JSONDecodeError`) → запись ошибки в лог и пустой список. В любом случае программа не падает.
- `"id": len(contacts) + 1` — простой уникальный идентификатор. (Обратите внимание: если контакт удалить, следующий идентификатор может повторить существующий. В настоящей базе данных в темах бэкенда и баз данных база сама выдаёт идентификаторы, и это как раз одна из причин её использовать.)
- **Загрузить → изменить в памяти → сохранить**: базовый шаблон работы с файлом данных.

### Строка JSON — `dumps` / `loads`

Файл есть не всегда. В сети, например, вы получаете **строку** JSON:

```python
import json

response_text = '{"id": 7, "name": "Dana", "active": true, "tags": ["new", "vip"]}'

student = json.loads(response_text)
print(student["name"], student["active"], student["tags"][1])

reply = {"status": "ok", "count": 3, "error": None}
print(json.dumps(reply))
```

```text
Dana True vip
{"status": "ok", "count": 3, "error": null}
```

Именно это FastAPI будет делать за нас в темах бэкенда и баз данных: `loads` для входящего запроса и `dumps` для исходящего ответа.

## Типичная ошибка

**Ошибка 1: недопустимый JSON, написанный вручную.**

```python
json.loads("{'name': 'Dana'}")
```

```text
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)
```

В JSON — только **двойные кавычки**. Частая причина: скопировали вывод `print(dict)` из Python, где используются одинарные кавычки, поэтому это **не** допустимый JSON. Правильный способ создать JSON — всегда `json.dumps`, а не `str(dict)`.

**Ошибка 2: путать `dump` и `dumps`.**

```python
with open(path, "w", encoding="utf-8") as file:
    json.dumps(data, file)
```

```text
TypeError: dumps() takes 1 positional argument but 2 were given
```

Для файла — `dump` (без s). `dumps` возвращает строку и никуда не пишет.

**Ошибка 3: сохранять то, чего JSON не знает.**

```python
json.dumps({"tags": {"a", "b"}})
```

```text
TypeError: Object of type set is not JSON serializable
```

В JSON нет множества (и нет tuple: tuple **сохраняется как массив** и загружается обратно как **list**). Сначала преобразуйте: `list(my_set)`. То же самое произойдёт в теме ООП с объектами классов, и там мы научимся превращать их в dict.

**Ошибка 4: числовые ключи возвращаются строками.**

```python
data = {1: "one"}
loaded = json.loads(json.dumps(data))
print(loaded)        # {'1': 'one'}
print(loaded[1])     # KeyError: 1
```

В JSON ключи **всегда строки**, поэтому `1` стало `"1"`. Обращайтесь через `loaded["1"]` или заранее планируйте текстовые ключи.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Практика

Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.

### Упражнение 1: Из Python в JSON

Для каждого значения Python — как оно будет выглядеть в JSON?

```text
True        None        "Dana"        [1, 2]        {"age": 24}
```

{{< solution >}}
| Python | JSON |
|---|---|
| `True` | `true` |
| `None` | `null` |
| `"Dana"` | `"Dana"` — всегда двойные кавычки |
| `[1, 2]` | `[1, 2]` — массив (array) |
| `{"age": 24}` | `{"age": 24}` — объект (object); ключи всегда строки |
{{< /solution >}}

### Упражнение 2: Сохранение и загрузка

Сохраните словарь в файл `profile.json` (с отступами и без кодирования неанглийского текста), загрузите его обратно и напечатайте имя и самую высокую оценку.

```py
profile = {"name": "Dana", "grades": [90, 85, 100]}
```

**Ожидаемый вывод:**

```text
Dana
100
```

{{< solution >}}
```python
import json

profile = {"name": "Dana", "grades": [90, 85, 100]}

with open("profile.json", "w", encoding="utf-8") as file:
    json.dump(profile, file, indent=2, ensure_ascii=False)

with open("profile.json", "r", encoding="utf-8") as file:
    loaded = json.load(file)

print(loaded["name"])
print(max(loaded["grades"]))
```

```text
Dana
100
```

- `dump` / `load` — с **файлом**.
- `indent=2` — файл, удобный для чтения людьми. `ensure_ascii=False` — неанглийский текст сохраняется как есть.
{{< /solution >}}

### Упражнение 3: Строка JSON

Следующий ответ пришёл от сервера в виде строки. Преобразуйте его в словарь и напечатайте имя и активен ли студент. Затем преобразуйте словарь `reply` в строку JSON.

```py
text = '{"id": 7, "name": "Dana", "active": true}'
reply = {"status": "ok", "error": None}
```

**Ожидаемый вывод:**

```text
Dana True
{"status": "ok", "error": null}
```

{{< solution >}}
```python
import json

text = '{"id": 7, "name": "Dana", "active": true}'
student = json.loads(text)
print(student["name"], student["active"])

reply = {"status": "ok", "error": None}
print(json.dumps(reply))
```

```text
Dana True
{"status": "ok", "error": null}
```

`loads` / `dumps` (с **s** — string) работают со **строкой**, а не с файлом. Так выглядит общение с сервером.
{{< /solution >}}

### Упражнение 4: Настройки, которые сохраняются

Напишите `load_settings(path)`, которая возвращает значения по умолчанию `{"theme": "light", "font_size": 14}`, а если файл существует — обновляет их согласно сохранённому в нём. Напишите также `save_settings(path, settings)`.

```python
print(load_settings("settings.json"))
save_settings("settings.json", {"theme": "dark"})
print(load_settings("settings.json"))
```

**Ожидаемый вывод:**

```text
{'theme': 'light', 'font_size': 14}
{'theme': 'dark', 'font_size': 14}
```

{{< solution >}}
```python
import json
from pathlib import Path

DEFAULTS = {"theme": "light", "font_size": 14}


def load_settings(path):
    settings = DEFAULTS.copy()
    if Path(path).exists():
        with open(path, "r", encoding="utf-8") as file:
            saved = json.load(file)
        for key, value in saved.items():
            settings[key] = value
    return settings


def save_settings(path, settings):
    with open(path, "w", encoding="utf-8") as file:
        json.dump(settings, file, indent=2)


print(load_settings("settings.json"))
save_settings("settings.json", {"theme": "dark"})
print(load_settings("settings.json"))
```

```text
{'theme': 'light', 'font_size': 14}
{'theme': 'dark', 'font_size': 14}
```

`DEFAULTS.copy()` — чтобы не менять сами значения по умолчанию (помните ссылки из урока об изменяемости?).
{{< /solution >}}

### Упражнение 5: Список контактов

Напишите две функции: `add_contact(path, name, phone)`, которая добавляет контакт в список, сохранённый в файле JSON (и создаёт файл, если его нет), и `find_phone(path, name)`, которая возвращает телефон или `None`.

```python
add_contact("contacts.json", "Dana", "050-1111111")
add_contact("contacts.json", "Avi", "052-2222222")
print(find_phone("contacts.json", "Avi"))
print(find_phone("contacts.json", "Noa"))
```

**Ожидаемый вывод:**

```text
052-2222222
None
```

{{< solution >}}
```python
import json
from pathlib import Path


def load_contacts(path):
    if not Path(path).exists():
        return []
    with open(path, "r", encoding="utf-8") as file:
        return json.load(file)


def add_contact(path, name, phone):
    contacts = load_contacts(path)
    contacts.append({"name": name, "phone": phone})
    with open(path, "w", encoding="utf-8") as file:
        json.dump(contacts, file, indent=2, ensure_ascii=False)


def find_phone(path, name):
    for contact in load_contacts(path):
        if contact["name"] == name:
            return contact["phone"]
    return None


add_contact("contacts.json", "Dana", "050-1111111")
add_contact("contacts.json", "Avi", "052-2222222")
print(find_phone("contacts.json", "Avi"))
print(find_phone("contacts.json", "Noa"))
```

```text
052-2222222
None
```

Файл хранит **список словарей** — ровно структуру из урока о dict. Каждое изменение: загрузить, изменить, сохранить.
{{< /solution >}}

<!-- exercises:end -->

## Итоги

- **JSON** = стандартный текстовый формат для структурированных данных. object ↔ `dict`, array ↔ `list`, `true`/`false`/`null` ↔ `True`/`False`/`None`.
- Правила: только **двойные** кавычки, без запятой в конце, без комментариев. Ключи — всегда строки.
- **Файл:** `json.dump(data, file)` / `json.load(file)`. **Строка:** `json.dumps(data)` / `json.loads(text)`.
- `indent=2` — для читаемости, `ensure_ascii=False` — для неанглийского текста.
- Шаблон: **загрузить (со значением по умолчанию) → изменить в памяти → сохранить**. Обрабатывайте отсутствующий файл и `JSONDecodeError`.
- Множества и объекты нужно преобразовать перед сохранением. JSON — язык каждого API в темах бэкенда и баз данных.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Официальная документация

- [Saving structured data with json](https://docs.python.org/3/tutorial/inputoutput.html#saving-structured-data-with-json)
- [json.dump()](https://docs.python.org/3/library/json.html#json.dump)
- [json.load()](https://docs.python.org/3/library/json.html#json.load)

<!-- docs:end -->
