Lire du JSON

Les six types de JSON et les valeurs qui font trébucher les tests.

La plupart des API que vous testerez envoient et reçoivent du JSON. C’est un format texte pour les données structurées, assez petit pour s’apprendre en un après-midi. La syntaxe pose rarement problème. Le problème, c’est que JSON laisse certaines choses non définies, et que chaque programme comble ces trous à sa façon. Beaucoup de bugs vivent dans ces trous, c’est pourquoi cette leçon leur consacre plus de temps qu’à la syntaxe.

Six sortes de valeurs

Une valeur JSON est toujours l’une de celles-ci :

TypeExempleRemarques
Objet{ "key": "value" }Des champs nommés entre accolades. Les clés sont toujours des chaînes entre guillemets doubles.
Tableau[1, 2, 3]Une liste ordonnée entre crochets. Les éléments peuvent être de n’importe quel type, mélangés.
Chaîne"hello"Du texte entre guillemets doubles. Les guillemets simples ne sont pas autorisés.
Nombre42, -7, 3.14159, 6.022e23Un seul type pour les entiers et les décimaux. Pas de zéros en tête, pas de NaN, pas d’Infinity.
Booléentrue, falseEn minuscules, sans guillemets.
NullnullEn minuscules, sans guillemets. Signifie « pas de valeur ».

C’est tout le système de types. Il n’y a pas de type date, pas de type entier distinct des décimaux, et aucun moyen d’ajouter des commentaires. Cette API a un point d’accès qui renvoie un exemple de chaque :

GET/api/edge-cases/types
Cette requête utilise un point d’accès réservé aux membres connectés.Se connecterCréer un compte gratuit
JSON
{
  "string": "hello",
  "emptyString": "",
  "integer": 42,
  "negativeInteger": -7,
  "float": 3.14159,
  "exponent": 6.022e23,
  "booleanTrue": true,
  "booleanFalse": false,
  "nullValue": null,
  "array": [1, 2, 3],
  "emptyArray": [],
  "object": { "key": "value" },
  "emptyObject": {},
  "arrayOfObjects": [ { "id": 1, "name": "Alice" }, { "id": 2, "name": "Bob" } ],
  "nestedArrays": [[1, 2], [3, [4, [5]]]],
  "stringThatLooksLikeNumber": "123",
  "stringThatLooksLikeBoolean": "true",
  "stringThatLooksLikeNull": "null"
}

Regardez les trois derniers champs. "123", "true" et "null" sont tous des chaînes, parce qu’ils sont entre guillemets. Une personne qui lit la réponse voit un nombre, un booléen et un null. Un programme voit trois chaînes. Quand vous vérifiez une réponse, vérifiez le type autant que la valeur. Une API qui renvoie "price": "249.99" un jour et "price": 249.99 le lendemain a changé son contrat, même si le nombre a l’air identique.

Désigner une valeur

Quand vous écrivez un test ou un rapport de bug, il vous faut un moyen de dire de quelle valeur vous parlez. Voici une partie du produit de la première leçon :

JSON
{
  "id": 1,
  "title": "Wireless Noise-Cancelling Headphones",
  "price": 249.99,
  "category": "electronics",
  "rating": { "rate": 4.3, "count": 127 },
  "inStock": true,
  "createdAt": "2025-06-12T10:30:00Z"
}

La notation informelle habituelle utilise un point pour entrer dans un objet, et des crochets avec un nombre pour choisir un élément d’un tableau. On compte à partir de zéro.

CheminValeur
title"Wireless Noise-Cancelling Headphones"
rating.rate4.3
rating.count127

Quand toute la réponse est un tableau, comme pour /api/Products?limit=2, l’ID du premier produit est [0].id et la note du second est [1].rating.rate. Les outils l’écrivent un peu différemment. JSONPath, que vous verrez dans Postman, REST Assured et beaucoup de bibliothèques de test, commence par un signe dollar pour la racine : $.rating.rate, $[0].id. Il a aussi des jokers : $[*].priceveut dire "le prix de chaque élément". JMESPath, utilisé par les outils en ligne de commande d’AWS, écrit la même chose [*].price. Inutile d’apprendre l’un ou l’autre en profondeur pour l’instant. Savoir que $[0].idveut dire "l’id du premier élément" suffit pour lire la plupart du code de test.

Null, absent et vide

Ils se ressemblent et veulent dire des choses différentes. Supposons qu’un utilisateur n’ait pas de deuxième prénom. Une API pourrait le dire d’au moins cinq façons :

  • "middleName": null, le champ existe et n’a pas de valeur
  • pas de clé middleName du tout
  • "middleName": "", une chaîne vide
  • "middleName": "null", le mot null en quatre lettres, en général un bug où quelque chose a appelé toString sur un null
  • "middleName": "None" ou "undefined", le même bug venu de Python ou de JavaScript

Ce point d’accès les renvoie tous côte à côte :

GET/api/edge-cases/nulls
Cette requête utilise un point d’accès réservé aux membres connectés.Se connecterCréer un compte gratuit
JSON
{
  "nullValue": null,
  "nestedNull": { "a": null, "b": { "c": null } },
  "arrayOfNulls": [null, null, null],
  "arrayWithNull": [1, null, 3],
  "nullAsString": "null",
  "undefinedAsString": "undefined",
  "noneAsString": "None",
  "emptyString": "",
  "emptyArray": [],
  "emptyObject": {},
  "zero": 0,
  "false": false
}

Lequel est correct dépend de la documentation de l’API, qui souvent ne le dit pas. Ce que vous pouvez tester, c’est la cohérence. Si un produit a "discount": null et qu’un autre n’a pas de clé discount, un client qui lit product.discount.amount plantera sur l’un et pas sur l’autre. Méfiez-vous aussi de 0 et de falseutilisés pour dire "inconnu". Un stock de 0 et un stock que personne ne connaît sont des faits différents.

Les nombres

JSON ne limite ni la taille ni la précision d’un nombre. Les programmes qui lisent du JSON, si. JavaScript stocke chaque nombre comme un flottant 64 bits, qui ne représente exactement les entiers que jusqu’à 9 007 199 254 740 991. Au-delà, il arrondit en silence.

GET/api/edge-cases/numbers
Cette requête utilise un point d’accès réservé aux membres connectés.Se connecterCréer un compte gratuit
JSON (réduit)
{
  "int64Max": 9223372036854775807,
  "int64MaxAsString": "9223372036854775807",
  "maxSafeInteger": 9007199254740991,
  "beyondSafeInteger": 9007199254740993,
  "beyondSafeIntegerAsString": "9007199254740993",
  "pointOnePlusPointTwo": 0.30000000000000004,
  "money": 19.99,
  "moneyAsString": "19.99",
  "trailingZeros": 1.50,
  "negativeZero": -0,
  "numberAsString": "42",
  "nanAsString": "NaN"
}

Analysez cette réponse en JavaScript et en Python, et comparez :

JavaScript
JSON.parse(body).beyondSafeInteger   // 9007199254740992
JSON.parse(body).int64Max            // 9223372036854776000
Python
json.loads(body)["beyondSafeInteger"]   # 9007199254740993
json.loads(body)["int64Max"]            # 9223372036854775807

JavaScript a transformé 9007199254740993 en 9007199254740992 sans la moindre erreur. Python l’a gardé exact. Si une API utilise des ID entiers sur 64 bits (ceux de Twitter l’étaient, comme ceux de beaucoup de bases de données), un client JavaScript peut finir par demander le mauvais enregistrement. C’est pourquoi les API soigneuses envoient les grands ID sous forme de chaînes, comme int64MaxAsString ci-dessus. Quand vous testez une API avec de grands ID numériques, vérifiez ce qu’un client navigateur reçoit réellement.

Les décimaux ont un problème voisin. 0.1 + 0.2 vaut 0.30000000000000004 dans presque tous les langages, parce que les flottants binaires ne peuvent pas représenter 0,1 exactement. Pour l’argent, cela compte. Certaines API envoient les prix comme des nombres (19.99), d’autres comme des chaînes ("19.99"), d’autres comme des centimes entiers (1999). Chacune peut fonctionner. Un test doit vérifier que les totaux tombent juste au centime près, pas qu’ils sont "assez proches".

Deux détails de la même réponse. 1.50 et 1.5 sont le même nombre une fois analysés, donc un test qui compare le texte brut échoue là où un test qui compare les valeurs passe. Et -0 est du JSON valide, mais le JSON.stringify(-0) de JavaScript écrit 0 : il ne survit donc pas à un aller-retour.

Les dates sont des chaînes

JSON n’a pas de type date : les dates voyagent donc sous forme de chaînes ou de nombres, et chaque API choisit son format. Voici un même instant écrit de plusieurs façons :

GET/api/edge-cases/dates
Cette requête utilise un point d’accès réservé aux membres connectés.Se connecterCréer un compte gratuit
JSON (réduit)
{
  "iso8601Utc": "2025-07-15T09:30:00Z",
  "iso8601PositiveOffset": "2025-07-15T15:00:00+05:30",
  "iso8601NoZone": "2025-07-15T09:30:00",
  "dateOnly": "2025-07-15",
  "unixSeconds": 1752571800,
  "unixMilliseconds": 1752571800000,
  "usFormat": "07/15/2025",
  "euFormat": "15/07/2025",
  "invalidLeapDay": "2025-02-29T00:00:00Z",
  "nullDate": null,
  "emptyDate": ""
}

Le format que vous verrez le plus est ISO 8601 avec fuseau, comme 2025-07-15T09:30:00Z. Le Z signifie UTC. Ce qu’il faut vérifier quand une API renvoie des dates :

  • Le fuseau est présent. 2025-07-15T09:30:00 sans fuseau veut dire 9 h 30 quelque part, et chaque client devinera un quelque part différent.
  • Les timestamps Unix précisent s’il s’agit de secondes ou de millisecondes. 1752571800 et 1752571800000 sont le même instant, et les confondre vous envoie en 1970 ou en l’an 57500.
  • 07/15/2025 et 15/07/2025 ne sont sans ambiguïté que parce que 15 ne peut pas être un mois. 03/04/2025 est mars ou avril selon qui le lit.
  • Les dates impossibles comme 2025-02-29 sont acceptées par certains analyseurs et refusées par d’autres. Essayez de les envoyer à une API qui prend des dates en entrée.

Ordre des clés et clés en double

Les clés d’un objet n’ont pas d’ordre. {"a": 1, "b": 2} et {"b": 2, "a": 1} sont le même objet. La plupart des serveurs envoient les clés toujours dans le même ordre, d’où la tentation de comparer toute la réponse comme une chaîne. Ce test casse le jour où une mise à jour de bibliothèque réordonne les clés, alors que rien n’est faux. Comparez plutôt les valeurs analysées. L’ordre des tableaux, en revanche, compte : [1, 2] et [2, 1] sont des tableaux différents.

Les clés en double sont un cas plus étrange. La norme JSON dit que les clés "devraient" être uniques, pas qu’elles doivent l’être, donc ceci est techniquement permis :

GET/api/edge-cases/duplicate-keys
Cette requête utilise un point d’accès réservé aux membres connectés.Se connecterCréer un compte gratuit
JSON
{
  "id": 1,
  "name": "first value",
  "name": "second value",
  "nested": { "flag": true, "flag": false },
  "id": 2
}

Le JSON.parse de JavaScript et le json.loads de Python gardent tous les deux la dernière valeur, ce qui donne {"id": 2, "name": "second value", "nested": {"flag": false}}. D’autres analyseurs gardent la première valeur ou refusent le document. Si une passerelle vérifie la première clé role et que le backend lit la seconde, cette différence devient une faille de sécurité. Vous croiserez rarement des clés en double dans les réponses, mais cela vaut la peine d’en envoyer dans les requêtes quand vous testez la gestion des entrées.

Quand le JSON est cassé

Parfois, un serveur envoie quelque chose qui n’est même pas du JSON valide : un corps tronqué, une page d’erreur HTML, une trace de pile. Voici une réponse qui prétend être du JSON et n’en est pas :

Réponse
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id": 1, "name": "Alice", "tags": ["a", "b",], "active": tru

Il y a une virgule finale après "b", et le corps s’arrête au milieu de true. Le statut est 200 et l’en-tête Content-Type dit application/json : un client essaiera donc de l’analyser et échouera. Node.js signale Unexpected token ']'. Python signale Illegal trailing comma before end of array. Dans une vraie API, cela veut généralement dire que le serveur a planté en pleine écriture de la réponse, ou qu’un proxy l’a coupée. La leçon pour les tests : analysez le corps à chaque vérification. Un 200 avec un corps impossible à analyser est un échec, et un test qui ne regarde que le code de statut le manquera.

Lire une grosse réponse

Les vraies réponses font souvent des centaines de lignes. Avant de vérifier les détails, repérez la forme : le niveau supérieur est-il un objet ou un tableau, combien y a-t-il d’éléments, quels champs a un élément. Puis lisez un élément en entier. La plupart des bugs JSON se voient à la seule forme, comme une liste qui revient sous forme d’objet unique quand il n’y a qu’un résultat, ou un objet enveloppé dans une clé data supplémentaire sur un point d’accès et pas sur un autre.

Exercice

Envoyez GET /api/Countries/code/INconnectez-vous pour voir et répondez à ces questions à partir de la réponse, en écrivant chaque chemin avec la notation à points de cette leçon :

  • De quel type est population, et de quel type est callingCode ? Pourquoi pourraient-ils différer ?
  • Quel est le chemin vers le symbole de la devise ?
  • Quel est le chemin vers la deuxième langue ?
  • Le champ flag est un emoji. Regardez la réponse brute avec curl et comparez-la avec ce qu’affiche votre navigateur. Qu’est-ce qui diffère, et est-ce important pour un client ?

Ouvrez ensuite /api/edge-cases/stringsconnectez-vous pour voir et trouvez au moins deux valeurs qui sembleraient correctes dans un navigateur mais pourraient casser un programme qui les stocke ou les affiche.

S’entraîner et aller plus loin