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 | +------------------+-------------+
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
ssignifie string. Les fonctionsdumps()etloads()travaillent directement avec des chaînes de caractères en mémoire -
Les fonctions sans
stravaillent 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
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]}
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" ] } """
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
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 ! 😎")
À 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
Il faut savoir que l'encodage par défaut pour open() dépend du système d'exploitation :
-
Windows : bien souvent
cp1252oumbcs(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}")
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" }
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
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
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}
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
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'>
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
OrderedDictpour garantir l'ordre (n'hésitez pas à consulter notre article sur le modulecollections)
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'>
- 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 passantlistàobject_pairs_hookpour 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)]
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))
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)
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>
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.