# JSON: קריאה, כתיבה ושמירת נתונים<no value>

## מה נלמד?

- מהו **JSON** ואיך הוא נראה.
- איך JSON מתאים ל-**dict** ול-**list** של Python.
- **שמירה** לקובץ וטעינה ממנו.
- **המרה** בין מחרוזת JSON לנתונים.

## למה צריך את זה?

בשיעור הקודם שמרנו טקסט, ולשם כך המצאנו "פורמט" משלנו: `Dana,90` בכל שורה. עכשיו נניח שלכל סטודנט יש **רשימת** ציונים, **כתובת** עם עיר ורחוב, ו**דגל** `is_active`. איך שומרים את זה בשורת טקסט? כל אחד ימציא פורמט אחר, ונצטרך לכתוב קוד פענוח מסובך.

**JSON** הוא פורמט **סטנדרטי** לנתונים מובנים. כל שפת תכנות יודעת לקרוא ולכתוב אותו. ובעיקר:

- **נושא OOP** — הפרויקט יישמר כ-JSON.
- **נושא Backend ומסדי נתונים** — **כל** בקשה ותשובה ב-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` באותיות **קטנות**.
- **אין פסיק** אחרי האיבר האחרון.
- **אין הערות**.
- מפתחות של object הם **תמיד מחרוזות**.

### Serialization ו-Deserialization

| מונח | כיוון | פונקציה |
|---|---|---|
| **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 בזיכרון) אי אפשר להעביר כמו שהיא בדלת. **Serialization** = לפרק אותה ולארוז בקרטון שטוח עם הוראות הרכבה (טקסט JSON), שאפשר לשמור במחסן (קובץ) או לשלוח במשאית (רשת). **Deserialization** = להרכיב אותה מחדש בבית החדש, ולקבל ספה זהה.
{{< /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` — טקסט שאינו באנגלית (עברית למשל) נשמר כמו שהוא, ולא כקודים כמו `ד`.

## דוגמת קוד ראשונה

`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, ...)` | **Serialization**: הופך את ה-dict לטקסט JSON וכותב לקובץ. `True` הפך ל-`true`, ו-`None` ל-`null`. |
| `json.load(file)` | **Deserialization**: קורא את הטקסט ובונה ממנו dict חדש. |
| `type(loaded)` | `dict` רגיל לגמרי. אפשר לעבוד איתו כמו עם כל dict. |
| `loaded["address"]["city"]` | גישה למבנה מקונן, כמו בנושא מבני נתונים ופונקציות. |
| `loaded == profile` | `True`: הנתונים שרדו את "הסיבוב" לקובץ וחזרה בשלמותם. (אבל `loaded is profile` הוא `False`, כי זה אובייקט חדש.) |

## דוגמה נוספת

**"מסד נתונים" קטן בקובץ JSON** — טעינה עם ברירת מחדל, הוספה, שמירה וטיפול בקובץ פגום. בדיוק התבנית שתשמש בפרויקט של נושא OOP. `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
```

**פירוק:**

- **הקובץ מחזיק list של dicts**: בדיוק "טבלה" של רשומות.
- `load_contacts` מטפלת בשני מקרים: **אין קובץ** (הרצה ראשונה) ‏←‏ רשימה ריקה. **קובץ פגום** (`json.JSONDecodeError`) ‏←‏ רישום שגיאה ורשימה ריקה. בכל מקרה התוכנית לא קורסת.
- `"id": len(contacts) + 1` — מזהה ייחודי פשוט. (שימו לב: אם נמחק איש קשר, המזהה הבא יכול לחזור על מזהה קיים. ב-Database אמיתי בנושא Backend ומסדי נתונים, המסד ייתן מזהים בעצמו, וזו בדיוק אחת הסיבות שמשתמשים בו.)
- **טעינה ‏←‏ שינוי בזיכרון ‏←‏ שמירה**: התבנית הבסיסית של עבודה עם קובץ נתונים.

### מחרוזת 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 יעשה בשבילנו בנושא Backend ומסדי נתונים: `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 אין set (ואין tuple: tuple **נשמר כ-array** ונטען בחזרה כ-**list**). ממירים קודם: `list(my_set)`. אותו דבר יקרה בנושא OOP עם אובייקטים של מחלקות, ושם נלמד להמיר אותם ל-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]` — מערך |
| `{"age": 24}` | `{"age": 24}` — אובייקט; המפתחות תמיד מחרוזות |
{{< /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()` — כדי לא לשנות את ברירות המחדל עצמן (זוכרים את ההפניות מהשיעור על Mutability?).
{{< /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`.
- set ואובייקטים צריך להמיר לפני השמירה. JSON הוא השפה של כל API בנושא Backend ומסדי נתונים.

<!-- 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 בקצרה
- [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 -->
