# JSON : lire, écrire et enregistrer des données<no value>

## Qu'allons-nous apprendre ?

- Ce qu'est **JSON** et à quoi il ressemble.
- Comment JSON correspond aux **dict** et **list** de Python.
- **Enregistrer** dans un fichier et charger depuis un fichier.
- **Convertir** entre une chaîne JSON et des données.

## Pourquoi en avons-nous besoin ?

Dans la leçon précédente, nous avons enregistré du texte, et pour cela nous avons inventé notre propre « format » : `Dana,90` sur chaque ligne. Supposons maintenant que chaque étudiant ait une **liste** de notes, une **adresse** avec une ville et une rue, et un **indicateur** `is_active`. Comment enregistrer cela dans une ligne de texte ? Chacun inventera un format différent, et il faudra écrire un code d'analyse compliqué.

**JSON** est un format **standard** pour les données structurées. Tous les langages de programmation savent le lire et l'écrire. Et surtout :

- **Le thème POO** — le projet sera enregistré en JSON.
- **Les thèmes backend et bases de données** — **chaque** requête et chaque réponse d'une API est envoyée en JSON. FastAPI convertit automatiquement un dict Python en JSON et inversement.

## Explication à partir des bases

### Qu'est-ce que JSON ?

**JSON** = **J**ava**S**cript **O**bject **N**otation. Il est né dans JavaScript, mais c'est aujourd'hui un format général. Un fichier JSON est un **fichier texte** à la structure définie, qui ressemble presque exactement aux dict et list de Python :

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

### La correspondance entre JSON et Python

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

### Les règles importantes de JSON (différentes de Python !)

- Les chaînes et les clés **uniquement entre guillemets doubles** `"..."`. Les guillemets simples `'...'` ne sont pas valides.
- `true`, `false`, `null` en **minuscules**.
- **Pas de virgule** après le dernier élément.
- **Pas de commentaires**.
- Les clés d'un objet sont **toujours des chaînes**.

### La sérialisation et la désérialisation

| Terme | Sens | Fonction |
|---|---|---|
| **Sérialisation** (Serialization) | Données Python → texte JSON | `json.dump` / `json.dumps` |
| **Désérialisation** (Deserialization) | Texte JSON → données Python | `json.load` / `json.loads` |

**Comment s'en souvenir :** le **s** à la fin (`dumps`, `loads`) = **s**tring, on travaille avec une **chaîne**. Sans s (`dump`, `load`) = on travaille avec un **fichier**.

{{< box "analogy" "Analogie : emballer des meubles pour un déménagement" >}}
Un canapé dans le salon (un dict en mémoire) ne passe pas la porte tel quel. **La sérialisation** = le démonter et l'emballer dans un carton plat avec une notice de montage (le texte JSON), que l'on peut garder dans une réserve (un fichier) ou envoyer dans un camion (le réseau). **La désérialisation** = le remonter dans la nouvelle maison, et obtenir un canapé identique.
{{< /box >}}

## Syntaxe

```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` — écrit un JSON « joli », avec des sauts de ligne et une indentation. Sans lui, tout est sur une seule ligne.
- `ensure_ascii=False` — un texte qui n'est pas en anglais (hébreu, caractères accentués, par exemple) est enregistré tel quel, et non sous forme de codes d'échappement (une barre oblique inverse, `u` et quatre chiffres hexadécimaux pour chaque lettre).

## Premier exemple de code

`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)
```

**Sortie :**

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

**Le `profile.json` qui a été créé :**

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

## Analyse du code ligne par ligne

| Ligne | Explication |
|---|---|
| `import json` | Un module standard. Aucune installation nécessaire. |
| `json.dump(profile, file, indent=2, ...)` | **Sérialisation** : transforme le dict en texte JSON et l'écrit dans le fichier. `True` est devenu `true`, et `None` est devenu `null`. |
| `json.load(file)` | **Désérialisation** : lit le texte et construit un nouveau dict à partir de lui. |
| `type(loaded)` | Un `dict` tout à fait ordinaire. On peut travailler avec lui comme avec n'importe quel dict. |
| `loaded["address"]["city"]` | Accès à une structure imbriquée, comme dans le thème structures de données et fonctions. |
| `loaded == profile` | `True` : les données ont survécu intégralement à « l'aller-retour » vers le fichier. (Mais `loaded is profile` vaut `False`, car c'est un nouvel objet.) |

## Un autre exemple

**Une petite « base de données » dans un fichier JSON** — chargement avec une valeur par défaut, ajout, enregistrement et gestion d'un fichier corrompu. Exactement le modèle qui servira dans le projet de POO. `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()
```

**Première exécution :**

```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
```

**Deuxième exécution :**

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

**Analyse :**

- **Le fichier contient une liste de dicts** : exactement un « tableau » d'enregistrements.
- `load_contacts` gère deux cas : **pas de fichier** (la première exécution) → une liste vide. **Un fichier corrompu** (`json.JSONDecodeError`) → une erreur journalisée et une liste vide. Dans les deux cas, le programme ne plante pas.
- `"id": len(contacts) + 1` — un identifiant unique simple. (Remarquez : si un contact est supprimé, l'identifiant suivant risque de répéter un identifiant existant. Dans une vraie base de données, dans les thèmes backend et bases de données, c'est la base elle-même qui attribuera les identifiants, et c'est justement l'une des raisons de l'utiliser.)
- **Charger → modifier en mémoire → enregistrer** : le modèle de base du travail avec un fichier de données.

### Une chaîne JSON — `dumps` / `loads`

Il n'y a pas toujours de fichier. Sur le réseau, par exemple, on reçoit une **chaîne** 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}
```

C'est exactement ce que fera FastAPI pour nous dans les thèmes backend et bases de données : `loads` sur la requête entrante, et `dumps` sur la réponse sortante.

## Erreur courante

**Erreur 1 : un JSON invalide écrit à la main.**

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

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

En JSON, uniquement des **guillemets doubles**. La raison courante : copier la sortie de `print(dict)` de Python, qui utilise des guillemets simples, ce n'est donc **pas** du JSON valide. La bonne façon de créer du JSON est toujours `json.dumps`, et non `str(dict)`.

**Erreur 2 : confondre `dump` et `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
```

Pour un fichier, utilisez `dump` (sans s). `dumps` renvoie une chaîne, et n'écrit nulle part.

**Erreur 3 : enregistrer quelque chose que JSON ne connaît pas.**

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

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

JSON n'a pas de set (ni de tuple : un tuple **est enregistré comme un array** et rechargé sous forme de **liste**). Convertissez d'abord : `list(my_set)`. La même chose arrivera dans le thème POO avec les objets des classes, et nous y apprendrons à les convertir en dict.

**Erreur 4 : les clés numériques reviennent sous forme de chaînes.**

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

En JSON, les clés sont **toujours des chaînes**, donc `1` est devenu `"1"`. Accédez-y avec `loaded["1"]`, ou prévoyez dès le départ des clés textuelles.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Exercices

Les exercices n'utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d'abord seul, et n'ouvrez la solution qu'ensuite.

### Exercice 1: De Python à JSON

Pour chaque valeur Python — à quoi ressemblera-t-elle en JSON ?

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

{{< solution >}}
| Python | JSON |
|---|---|
| `True` | `true` |
| `None` | `null` |
| `"Dana"` | `"Dana"` — toujours des guillemets doubles |
| `[1, 2]` | `[1, 2]` — un array |
| `{"age": 24}` | `{"age": 24}` — un objet ; les clés sont toujours des chaînes |
{{< /solution >}}

### Exercice 2: Enregistrer et charger

Enregistrez le dictionnaire dans le fichier `profile.json` (avec une indentation, et sans encoder le texte non anglais), rechargez-le, et affichez le nom et la note la plus haute.

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

**Résultat attendu :**

```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` — avec un **fichier**.
- `indent=2` — un fichier lisible par des humains. `ensure_ascii=False` — le texte non anglais est enregistré tel quel.
{{< /solution >}}

### Exercice 3: Une chaîne JSON

La réponse suivante est arrivée d'un serveur sous forme de chaîne. Convertissez-la en dictionnaire et affichez le nom et si l'étudiant est actif. Convertissez ensuite le dictionnaire `reply` en chaîne JSON.

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

**Résultat attendu :**

```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` (avec un **s** — string) travaillent avec une **chaîne**, pas avec un fichier. C'est à cela que ressemble la communication avec un serveur.
{{< /solution >}}

### Exercice 4: Des réglages qui sont enregistrés

Écrivez `load_settings(path)` qui renvoie les valeurs par défaut `{"theme": "light", "font_size": 14}`, et si le fichier existe — les met à jour selon ce qui y est enregistré. Écrivez aussi `save_settings(path, settings)`.

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

**Résultat attendu :**

```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()` — pour ne pas modifier les valeurs par défaut elles-mêmes (vous vous souvenez des références de la leçon sur la mutabilité ?).
{{< /solution >}}

### Exercice 5: Une liste de contacts

Écrivez deux fonctions : `add_contact(path, name, phone)` qui ajoute un contact à une liste enregistrée dans un fichier JSON (et crée le fichier s'il n'existe pas), et `find_phone(path, name)` qui renvoie le téléphone ou `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"))
```

**Résultat attendu :**

```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
```

Le fichier contient **une liste de dictionnaires** — exactement la structure de la leçon sur dict. À chaque modification : charger, modifier, enregistrer.
{{< /solution >}}

<!-- exercises:end -->

## Résumé

- **JSON** = un format texte standard pour les données structurées. object ↔ `dict`, array ↔ `list`, `true`/`false`/`null` ↔ `True`/`False`/`None`.
- Règles : guillemets **doubles** uniquement, pas de virgule finale, pas de commentaires. Les clés sont toujours des chaînes.
- **Fichier :** `json.dump(data, file)` / `json.load(file)`. **Chaîne :** `json.dumps(data)` / `json.loads(text)`.
- `indent=2` pour la lisibilité, `ensure_ascii=False` pour un texte non anglais.
- Le modèle : **charger (avec une valeur par défaut) → modifier en mémoire → enregistrer**. Gérez un fichier manquant et `JSONDecodeError`.
- Les sets et les objets doivent être convertis avant l'enregistrement. JSON est la langue de toute API dans les thèmes backend et bases de données.

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

## Documentation officielle

- [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 -->
