Le module json

Apprenez à utiliser le module json de Python.

Publié le par Gabriel Trouvé (mis à jour le )

48 minutes

Pour stocker des informations, configurer une application, ou créer et interroger une API, le format JSON est incontournable. Python intègre le module json dans sa bibliothèque standard.

Prêt pour un tour du propriétaire ? Nous allons parler de ce qui est le plus utilisé, tout en allant dans le détail de la bibliothèque.

Qu'est-ce que le format JSON ?

Le JSON (JavaScript Object Notation) est un format de texte permettant le stockage et le transport de données. Il est aussi bien lisible pour nous, simples humains, que pour les machines.

Et honnêtement, si vous êtes habitués aux structures de données de Python, le JSON ne vous dépaysera pas : il ressemble à nos bons vieux dictionnaires et listes ! Cependant, attention à la casse et aux types.

D'ailleurs, voici comment Python traduit automatiquement les types lors de la sérialisation (écriture) et de la désérialisation :

+------------------+-------------+
| Type JSON        | Type Python |
+------------------+-------------+
| string           | str         |
| number (integer) | int         |
| number (real)    | float       |
| object           | dict        |
| array            | list        |
| true             | True        |
| false            | False       |
| null             | None        |
+------------------+-------------+
BASH

Attention

  • Le JSON exige impérativement des doubles guillemets pour ses chaînes de caractères

  • En Python, un tuple est sérialisé en tableau JSON. Ainsi, lors de la désérialisation pour revenir à votre objet d'origine, vous obtiendrez une liste et non un tuple

Clarification : json.loads vs json.load

Et on pourrait se poser aussi la question pour json.dumps et json.dump. Pourquoi écrire ces fonctions avec ou sans s ?

La règle est simple :

  • Le s signifie string. Les fonctions dumps() et loads() travaillent directement avec des chaînes de caractères en mémoire

  • Les fonctions sans s travaillent directement avec des fichiers

json.dumps() -> Sérialise un objet Python en chaîne str JSON
json.loads() -> Désérialise une chaîne str JSON en objet Python

json.dump()  -> Écrit un objet Python dans un fichier JSON
json.load()  -> Lit un fichier JSON pour le convertir en objet Python
PYTHON

Mais nous allons détailler tout cela dans les prochaines parties 😊.

Manipuler le JSON en mémoire : dumps() et loads()

Sérialiser avec json.dumps()

La sérialisation consiste à transformer un objet Python en une chaîne de caractères JSON. On utilise pour cela json.dumps() :

import json

# Notre ami Patrick et ses données
utilisateur = {
    "nom": "Patrick",
    "age": 35,
    "est_admin": True,
    "passions": ["Code", "Pétanque"],
    "coordonnees": (48.8566, 2.3522)  # Un tuple !
}

# Conversion en chaîne de caractères JSON
json_string = json.dumps(utilisateur)

print(type(json_string))  # Affiche : <class 'str'>
print(json_string)
# Le tuple converti en liste [] et le "é" transformé en code unicode :
# {"nom": "Patrick", "age": 35, "est_admin": true, "passions": ["Code", "P\u00e9tanque"], "coordonnees": [48.8566, 2.3522]}
PYTHON

On remarque que, par défaut, le JSON généré est compacté sur une seule ligne, ce qui, pour nous, simples humains, n'est pas forcément ce qu'il y a de plus lisible. Il existe plusieurs paramètres pour pallier cela : indent, sort_keys et ensure_ascii.

import json

utilisateur = {
    "nom": "Sébastien",
    "age": 30,
    "est_admin": False,
    "passions": ["Vélo", "Cinéma"]
}

json_formate = json.dumps(
    utilisateur,
    indent=4,              # Aligne proprement avec 4 espaces d'indentation
    sort_keys=True,        # Trie les clés par ordre alphabétique
    ensure_ascii=False     # Indispensable pour garder nos accents intacts
)

print(json_formate)

"""
{
    "age": 30,
    "est_admin": false,
    "nom": "Sébastien",
    "passions": [
        "Vélo",
        "Cinéma"
    ]
}
"""
PYTHON

Désérialiser une chaîne avec json.loads()

Pour transformer une chaîne JSON en objet Python, on utilise json.loads() :

import json

reponse_api = '{"nom": "Patrick", "role": "admin", "score": null}'

# Conversion de la chaîne JSON en dictionnaire Python
donnees = json.loads(reponse_api)

print(type(donnees))  # Affiche : <class 'dict'>
print(donnees["nom"]) # Affiche : Patrick
print(donnees["score"]) # Affiche : None
PYTHON

Travailler avec des fichiers : dump() and load()

Pour sauvegarder ou lire des fichiers physiques avec JSON, nous utilisons nos gestionnaires de contexte habituels.

Sauvegarder dans un fichier avec json.dump()

Pas de blabla, vous allez très vite comprendre comment cela fonctionne, et on va même utiliser les paramètres indent et ensure_ascii :

import json

configuration = {
    "theme": "sombre",
    "notifications_actives": True,
    "langue": "fr"
}

with open("config.json", "w", encoding="utf-8") as fichier:
    json.dump(configuration, fichier, indent=4, ensure_ascii=False)
# indent=4 pour une meilleure lisibilité, ensure_ascii=False pour permettre les caractères spéciaux

print("Fichier de configuration sauvegardé pour Patrick ! 😎")
PYTHON

À noter

Dans ce cas, nous sommes bien en mode écriture "w".

Charger un fichier avec json.load()

Maintenant, nous allons récupérer la configuration stockée précédemment. Il faut penser à ouvrir le fichier en mode "r" pour que json.load() se charge d'extraire les données :

import json

with open("config.json", "r", encoding="utf-8") as fichier:
    config_recuperee = json.load(fichier)

print(config_recuperee["theme"])  # Affiche : sombre
PYTHON

Il faut savoir que l'encodage par défaut pour open() dépend du système d'exploitation :

  • Windows : bien souvent cp1252 ou mbcs (selon la localisation)

  • Linux / macOS : bien souvent utf-8

C'est pour cette raison que nous sommes explicites sur l'encodage, ce qui permet d'éviter des UnicodeDecodeError.

À noter

Bonne nouvelle : la PEP 686 prévoit qu'UTF-8 soit la valeur par défaut avec l'arrivée de Python 3.15.

Gérer les erreurs de décodage

Comme, en bons développeurs consciencieux 🤓, on aime prévoir les imprévus, nous allons anticiper les cas où :

  • Le fichier est introuvable (Python lève alors une FileNotFoundError)

  • Le JSON est mal formaté (si, par exemple, un petit filou a modifié le fichier à la main)

import json

nom_fichier = "configuration.json"

try:
    with open(nom_fichier, "r", encoding="utf-8") as fichier:
        donnees = json.load(fichier)
    print("Données chargées avec succès !")

except FileNotFoundError:
    # On intercepte le cas où le fichier n'existe pas du tout
    print(f"Erreur : Le fichier '{nom_fichier}' est introuvable.")

except json.JSONDecodeError as erreur:
    # On intercepte le cas où le JSON contient des erreurs de syntaxe (mal formé)
    print("Erreur : Le fichier JSON est mal formé ou corrompu !")
    print(f"Détails de l'erreur : {erreur.msg} à la ligne {erreur.lineno}, colonne {erreur.colno}, position {erreur.pos}")
PYTHON

L'exception JSONDecodeError hérite de ValueError. Cependant, elle est bien plus étoffée et met à notre disposition plusieurs attributs pour localiser l'erreur dans un fichier JSON :

  • erreur.msg : le message d'erreur décrivant le problème

  • erreur.lineno : le numéro de ligne où l'erreur a été rencontrée

  • erreur.colno : le numéro de colonne exact

  • erreur.pos : l'index du caractère fautif

Si je reprends le code ci-dessus et que je « casse » le fichier configuration.json en ajoutant une deuxième virgule après sombre :

{
    "theme": "sombre",,
    "notifications_actives": true,
    "langue": "fr"
}
PYTHON

Si l'on lance le script :

Erreur : Le fichier JSON est mal formé ou corrompu !
Détails de l'erreur : Expecting property name enclosed in double quotes à la ligne 2, colonne 23, position 24
BASH

Pour aller plus loin

Rédaction oblige, j'aime approfondir les sujets et explorer les recoins cachés des modules que j'utilise. C'est d'ailleurs en écrivant ce genre d'article que j'en apprends le plus ! Voici donc quelques astuces un peu moins connues à utiliser avec le module json.

Ignorer les clés non autorisées

En Python, n'importe quel objet immuable et hachable peut servir de clé dans un dictionnaire. Cependant, le format JSON impose que toutes les clés soient des chaînes de caractères.
Si vous essayez de sérialiser un dictionnaire Python contenant des clés incompatibles avec le format JSON (comme un tuple), Python lèvera une TypeError. Le paramètre skipkeys=True permet d'ignorer silencieusement ces clés incompatibles pendant la sérialisation.

import json

# Un dictionnaire avec une clé invalide pour du JSON (un tuple)
donnees_bizarres = {
    "cle_valide": "Ok",
    (1, 2): "Erreur potentielle !",
}

# Sans skipkeys=True, la ligne ci-dessous plante !
resultat = json.dumps(donnees_bizarres, skipkeys=True)
print(resultat)  # Affiche : {"cle_valide": "Ok"}

# Sans le skipkeys=True, TypeError: keys must be str, int, float, bool or None, not tuple
PYTHON

Gérer les valeurs non standard

Prenons l'exemple de valeurs mathématiques particulières en Python, comme float('nan') (Not a Number, un résultat indéfini) ou float('inf') (l'infini).
Le format JSON interdit normalement ces valeurs : un nombre doit obligatoirement être un chiffre standard comme 42 ou -12.5.

Si nous sérialisons un dictionnaire contenant un float('nan'), il sera écrit tel quel, sans guillemets. C'est acceptable pour Python, mais pas forcément pour les autres langages qui vont recevoir notre JSON.

import json

donnees_scientifiques = {"valeur": float('nan')}


donnees_json = json.dumps(donnees_scientifiques)
print(donnees_json)
# {"valeur": NaN}
PYTHON

Autant vous dire que NaN n'est pas reconnu par le format JSON, contrairement à null ou true. Afin d'éviter de générer des données invalides, on peut utiliser le paramètre allow_nan=False. Ce dernier va forcer Python à respecter strictement la norme JSON en levant une ValueError, au lieu de produire un fichier corrompu.

import json

donnees_scientifiques = {"valeur": float('nan')}

try:
    # On force le respect strict du standard JSON
    json.dumps(donnees_scientifiques, allow_nan=False)
except ValueError as e:
    print(f"Refus de sérialisation : {e}")
    # Refus de sérialisation : Out of range float values are not JSON compliant: nan
PYTHON

Précision financière

Lorsque l'on désérialise un fichier JSON, il est possible de demander des objets Decimal à la place des float classiques. On utilise pour cela le paramètre parse_float=Decimal.

import json
from decimal import Decimal

json_facture = '{"total": 100.10}'

# On force l'utilisation de Decimal pour la précision financière
donnees = json.loads(json_facture, parse_float=Decimal)

print(type(donnees["total"]))  # Affiche : <class 'decimal.Decimal'>
PYTHON

Je vous invite à consulter cet article qui montre l'intérêt de l'objet Decimal.

Contrôler la structure

Depuis Python 3.7, l'ordre d'insertion des dictionnaires est préservé par défaut. Cependant, il existe le paramètre object_pairs_hook qui peut s'avérer utile :

  • Si votre code tourne avec une version antérieure à la 3.7, il est possible de forcer le décodage dans un OrderedDict pour garantir l'ordre (n'hésitez pas à consulter notre article sur le module collections)
import json
from collections import OrderedDict

json_donnees = '{"a": 1, "b": 2, "c": 3}'

donnees = json.loads(json_donnees, object_pairs_hook=OrderedDict)
print(type(donnees))  # Affiche : <class 'collections.OrderedDict'>
PYTHON
  • Même sur une version récente, on pourrait avoir besoin d'une structure sur mesure autre que le dictionnaire classique, comme une liste de tuples (clé, valeur). Imaginez devoir analyser un fichier JSON mal conçu contenant des clés dupliquées (j'imagine le pire, mais nous sommes prévoyants chez Docstring). Par défaut, un dictionnaire Python écrase les doublons. Le but est donc d'intercepter le flux en passant list à object_pairs_hook pour conserver toutes les données
import json

# Un JSON avec deux fois la clé "score", ce qui est valide mais peu recommandé
json_bizarre = '{"joueur": "Patrick", "score": 100, "score": 250}'

# La première valeur (100) est perdue
donnees_classiques = json.loads(json_bizarre)
print(donnees_classiques) 
# Affiche : {'joueur': 'Patrick', 'score': 250}

# On force l'extraction en liste de tuples
donnees_brutes = json.loads(json_bizarre, object_pairs_hook=list)

print(donnees_brutes)
# [('joueur', 'Patrick'), ('score', 100), ('score', 250)]
PYTHON

L'outil en ligne de commande

Le module json peut être exécuté directement depuis votre terminal pour valider ou formater un fichier JSON à la volée. Imaginez un fichier donnees.json illisible car écrit sur une seule ligne : ouvrez votre terminal et tapez python -m json.tool donnees.json.
Python affichera alors le contenu parfaitement indenté dans votre console. De plus, s'il y a une erreur de syntaxe, l'outil vous indiquera où elle se trouve.

Gérer les classes personnalisées

La méthode simple

Il est possible de passer une fonction de conversion au paramètre default. Dès que le module rencontrera un type qu'il ne connaît pas, il passera cet objet à votre fonction.

import json
from datetime import datetime

class Commande:
    def __init__(self, reference, date_creation):
        self.reference = reference
        self.date_creation = date_creation

# Une simple fonction de traduction
def traduire_commande(objet):
    if isinstance(objet, Commande):
        return {"ref": objet.reference, "date": objet.date_creation.isoformat()}

    raise TypeError(f"Le type {type(objet)} n'est pas sérialisable")

ma_commande = Commande("CMD-42", datetime.now())

# On passe notre fonction au paramètre 'default'
print(json.dumps(ma_commande, default=traduire_commande, indent=2))
PYTHON

Encodeur personnalisé

Nativement, le module json ne prend en charge que les types de base stricts. Dès qu'on lui passe un objet complexe issu de nos propres classes, il est perdu.

Pour sérialiser vos propres classes, il est recommandé de créer un encodeur personnalisé en héritant de json.JSONEncoder.

import json
from datetime import datetime

class Commande:
    def __init__(self, reference, date_creation):
        self.reference = reference
        self.date_creation = date_creation

# Notre classe d'encodage
class CommandeEncoder(json.JSONEncoder):
    def default(self, objet):
        if isinstance(objet, Commande):
            return {
                "reference": objet.reference,
                "date_creation": objet.date_creation.isoformat()
            }
        # Pour les types standards, on laisse faire l'encodeur parent via super()
        return super().default(objet)

ma_commande = Commande("CMD-42", datetime.now())

# On passe notre classe personnalisée au paramètre 'cls'
json_commande = json.dumps(ma_commande, cls=CommandeEncoder, indent=2)
print(json_commande)
PYTHON

Décodage d'objets

Pour faire le chemin inverse et transformer vos fichiers JSON en objets Python personnalisés au moment de la lecture, nous pouvons utiliser le paramètre object_hook.

import json

class Client:
    def __init__(self, nom, email):
        self.nom = nom
        self.email = email

    def __str__(self):
        return f"<Client {self.nom}>"

def json_vers_client(dictionnaire):
    if "nom" in dictionnaire and "email" in dictionnaire:
        return Client(dictionnaire["nom"], dictionnaire["email"])
    return dictionnaire

json_client = '{"nom": "Patrick", "email": "[email protected]"}'

# On récupère directement un objet Client
client_instancie = json.loads(json_client, object_hook=json_vers_client)
print(type(client_instancie))  # Affiche : <class '__main__.Client'>
print(client_instancie)        # Affiche : <Client Patrick>
PYTHON

Le paramètre attend une fonction : lors de l'analyse, cette dernière va intercepter le dictionnaire extrait du JSON pour le passer à votre fonction personnalisée.

Bravo, tu es prêt à passer à la suite

Rechercher sur le site

Inscris-toi à Docstring

Pour commencer ton apprentissage.

Tu as déjà un compte ? Connecte-toi.