JSON Toolkit

Points clés pour concevoir des contrats de données d'API JSON

L’essentiel : Six choses sont très difficiles à changer une fois livrées : les enveloppes de réponse, les formats d'erreur, la forme de pagination, la représentation des identifiants et du temps, la sémantique de null contre absent, et l'évolution de version.

Intermédiaire~13 min de lecturePublié le 2026-10-30Mis à jour le 2026-10-30
Illustration des points clés pour concevoir des contrats de données d'API JSON

L'essentiel : verrouillez l'enveloppe de réponse, le format d'erreur, la représentation des identifiants et du temps, et la sémantique null-contre-absent dans le contrat avant de débattre des détails de pagination et de nommage ; enveloppez toujours le niveau supérieur dans une enveloppe data pour que pagination et identifiants de traçage puissent être ajoutés plus tard sans casser les appelants existants.

Il n'y a pas de réponses standard en conception d'API, mais il y a des décisions « difficiles à reprendre une fois prises » qu'il vaut mieux avoir pesées avant d'écrire la première ligne de code.

1. Envelopper la réponse ou non

Trois styles :

// Style A: bare response
{ "id": 1, "name": "张三" }

// Style B: data envelope
{ "data": { "id": 1, "name": "张三" } }

// Style C: data + metadata
{ "data": { "id": 1, "name": "张三" }, "meta": { "requestId": "a1b2" } }
Style Avantages Inconvénients
Réponse nue Le plus simple, une couche de moins pour le frontend Impossible d'attacher des métadonnées ; un tableau au niveau supérieur porte des risques de détournement JSON et d'évolution
Enveloppe data Facile d'ajouter meta / errors / links Un niveau d'imbrication de plus, le frontend doit écrire res.data.data
Data + métadonnées Meilleure observabilité Exige un accord strict sur les champs qui appartiennent à meta

Recommandation : utilisez uniformément une enveloppe data pour les listes et les détails, et ne mettez jamais le niveau supérieur en tableau. Ainsi, ajouter plus tard de la pagination ou des identifiants de traçage ne casse jamais le contrat.

2. Format d'erreur : copiez la RFC 9457

Inventer sa propre structure d'erreur est une énorme source de coût de collaboration. La RFC 9457 (qui remplace la RFC 7807) définit application/problem+json :

{
  "type": "https://bbdrug.com/problems/insufficient-balance",
  "title": "余额不足",
  "status": 422,
  "detail": "账户余额 12.00 元,本次扣款需要 30.00 元",
  "instance": "/accounts/A1001/payments/p-889",
  "balance": "12.00",
  "required": "30.00"
}
Champ Signification Obligatoire
type URI stable du type de problème Recommandé
title Court résumé lisible par l'humain Recommandé
status Code de statut HTTP Recommandé
detail Explication spécifique de ce qui s'est passé cette fois Facultatif
instance L'instance de ressource qui a eu le problème Facultatif
Champs d'extension Champs métier arbitraires Facultatif

Les champs d'extension (comme balance / required ci-dessus) sont autorisés par la spécification, et le frontend peut les utiliser pour des messages précis au lieu de parser le texte de detail.

3. Pagination : le curseur bat l'offset

// Offset pagination
{ "data": [], "page": { "offset": 40, "limit": 20, "total": 137 } }

// Cursor pagination
{ "data": [], "page": { "nextCursor": "eyJpZCI6MTAwfQ", "hasMore": true } }
Dimension Pagination par offset Pagination par curseur
Difficulté d'implémentation Faible (LIMIT/OFFSET) Moyenne (nécessite une clé de tri stable)
Performance sur les pages profondes Mauvaise (plus l'offset est grand, plus c'est lent) Bonne
Quand les données changent Doublons ou éléments manquants Stable
Saut à une page Possible Impossible
Nécessite un total Exige un comptage supplémentaire Non requis

Recommandation : utilisez la pagination par offset pour les tables d'administration (elles ont besoin de sauts de page et de totaux), et la pagination par curseur pour les fils infinis destinés aux utilisateurs.

4. Identifiants et temps

4.1 Utilisez toujours des chaînes pour les identifiants

Le raisonnement a été couvert dans l'article sur les types : côté JavaScript, les valeurs au-delà de 2^53 perdent silencieusement de la précision. Même si l'identifiant actuel est un petit entier auto-incrémenté, l'utilisation de chaînes laisse la place de passer plus tard à des identifiants snowflake.

{ "id": "1000000000000000001" }

4.2 Utilisez des chaînes RFC 3339 pour le temps

{
  "createdAt": "2026-10-30T10:41:45+08:00",
  "updatedAt": "2026-10-30T02:41:45Z"
}
  • N'utilisez pas de nombres d'horodatage en millisecondes : ils sont difficiles à lire, et l'unité (secondes/millisecondes) est fréquemment confondue ;
  • Spécifiez explicitement le fuseau horaire : soit avec Z, soit avec un offset ; n'utilisez jamais « heure locale sans fuseau » ;
  • Nommez les champs uniformément en xxxAt pour qu'on voie au premier coup d'œil qu'il s'agit d'un instant.

5. null et absent

Vous devez choisir et l'écrire dans la documentation :

Approche Exprimer « pas de valeur » Exprimer « ne pas modifier »
Approche A La valeur du champ est null Omettre le champ dans la requête
Approche B Le champ n'existe pas Omettre le champ dans la requête

Le problème de l'approche A est qu'« omis » et « envoyé comme null » signifient des choses différentes dans un PATCH, ce qui est facile à mal interpréter ; le problème de l'approche B est que le JSON.stringify du frontend supprime les champs dont la valeur est undefined, ce qui correspond justement à la sémantique.

Recommandation pratique :

  • Utilisez l'approche A pour les corps de réponse (null signifie clairement pas de valeur, et le frontend n'a pas besoin de distinguer undefined) ;
  • Utilisez l'approche B pour les corps de requête (surtout PATCH), combinée avec Merge Patch ou JSON Patch pour rendre la sémantique explicite.

6. Nommage et structure des champs

Décision Choix courants Mise en garde
Style de nommage camelCase (écosystème JS) / snake_case (Python, bases de données) Uniforme dans le projet, ne pas mélanger
Valeurs d'énumération Minuscules avec underscores, "pending_review" N'utilisez pas de nombres ; ils sont difficiles à lire
Champs booléens isActive / enabled N'utilisez pas status: 0/1
Champs de tableau Noms pluriels, items Retournez [] pour les tableaux vides, jamais null
Profondeur d'imbrication Pas plus de 3 niveaux Au-delà, découpez ou proposez un paramètre expand

Retourner [] au lieu de null pour les tableaux vides compte beaucoup : data.items.map(...) côté frontend ne plantera pas à cause d'un null.

7. Stratégie d'évolution

Changement Rupture de compatibilité Comment gérer
Ajouter un champ facultatif Non Livrer directement
Ajouter un champ obligatoire Oui Ajouter une valeur par défaut, ou livrer par version
Supprimer un champ Oui Le marquer d'abord déprécié, observer les appelants, puis supprimer
Changer le type d'un champ Oui Ajouter un nouveau nom de champ et écrire les deux pendant la transition
Changer l'ensemble des valeurs d'énumération Dépend Le client doit avoir un repli pour les valeurs inconnues
Changer la sémantique d'un champ Oui Le traiter comme un nouveau champ

Trois règles de discipline :

  1. Les clients doivent tolérer les champs inconnus (ne vous enfermez pas avec additionalProperties: false) ;
  2. Les clients doivent avoir une branche de repli pour les valeurs d'énumération inconnues ;
  3. Conservez les champs dépréciés pendant au moins une version majeure, et marquez-les avec Deprecation dans les en-têtes de réponse ou la documentation.

8. Un exemple complet

{
  "data": [
    {
      "id": "1001",
      "name": "JSON 权威指南",
      "price": "89.50",
      "tags": ["json", "spec"],
      "publishedAt": "2026-09-01T00:00:00+08:00",
      "author": null
    }
  ],
  "page": { "nextCursor": null, "hasMore": false },
  "meta": { "requestId": "req-7f3a", "tookMs": 12 }
}

Cette réponse reflète toutes les conventions ci-dessus : enveloppe data, identifiants en chaînes, montants en chaînes, horodatages avec fuseau horaire, null signifiant absent, tableau vide, pagination par curseur, et meta pour les informations d'observabilité.

9. Liste de vérification

  • [ ] Le niveau supérieur n'est pas un tableau ; utilisez uniformément une enveloppe data ;
  • [ ] Les réponses d'erreur suivent la RFC 9457 et incluent un type stable ;
  • [ ] La forme de pagination correspond au scénario métier ;
  • [ ] Les identifiants sont des chaînes, et le temps utilise la RFC 3339 ;
  • [ ] La sémantique de null et d'absent est écrite dans la documentation ;
  • [ ] Le style de nommage est uniforme dans le projet, et les énumérations utilisent des chaînes minuscules ;
  • [ ] Le client a des replis pour les champs inconnus et les valeurs d'énumération inconnues ;
  • [ ] Les changements cassants passent par une version ou une transition à double écriture.

FAQ

Comment concevoir une API JSON évolutive ?

Enveloppez uniformément listes et détails dans une enveloppe data et ne mettez jamais le niveau supérieur en tableau, pour que pagination et identifiants de traçage puissent être ajoutés sans casser le contrat. Utilisez des identifiants en chaînes et des horodatages RFC 3339, et écrivez la sémantique null-contre-absent dans la documentation. Gardez les nouveaux champs facultatifs et sémantiquement indépendants, et donnez aux clients des replis pour les champs inconnus et les valeurs d'énumération.

Pourquoi ne pas inventer un format d'erreur personnalisé ?

Inventer sa propre structure d'erreur est une énorme source de coût de collaboration. Copiez application/problem+json de la RFC 9457, identifiez les problèmes avec un type stable, et autorisez des champs d'extension comme balance et required, pour que le frontend puisse construire des messages précis sans parser le texte de detail.

Faut-il utiliser la pagination par curseur pour tous les points de terminaison ?

Non. Les tables d'administration ont besoin de sauts de page et de totaux, donc la pagination par offset convient mieux ; les fils infinis destinés aux utilisateurs devraient utiliser la pagination par curseur, qui reste stable sur les pages profondes et évite doublons ou lacunes quand les données changent. Choisissez selon que le scénario a besoin de sauts de page et de totaux.