Échapper une chaîne pour JSON : trois caractères sont obligatoires, et un seul est un piège
Publié le 31/07/2026 · 13 min de lecture · Outils pour développeurs
Daniel Okonkwo — Développeur front-end et rédacteur Tech chez OneKitly
Performance web · Formats de fichiers
Vérifié à partir de 4 sources
La RFC 8259, section 7, ne nomme que trois choses à échapper obligatoirement dans une chaîne JSON : le guillemet droit, l'antislash et tout caractère de contrôle de U+0000 à U+001F. Tout le reste peut rester en UTF-8 littéral — lettres accentuées, idéogrammes, emoji, le caractère DEL en U+007F, le séparateur de ligne en U+2028. N'importe quel caractère peut être échappé, et c'est de là que vient l'habitude d'écrire \/ pour une barre oblique : JSON ne l'exige jamais, et la seule raison de le faire est l'inclusion de JSON dans une balise script HTML, où la suite </ doit être coupée. Il existe huit échappements de deux caractères — \" \\ \/ \b \f \n \r \t — et tout le reste passe par \uXXXX, quatre chiffres hexadécimaux, une unité UTF-16. Un caractère hors du plan multilingue de base en réclame deux : un emoji s'écrit en paire de surrogates sur douze caractères, jamais en échappement à six chiffres. C'est là qu'est le piège. Les chaînes JSON sont des suites d'unités UTF-16, et la grammaire autorise une unité qui n'est que la moitié d'une paire, sans partenaire — la spécification le dit en section 8.2, avec � pour exemple. Un tel texte JSON s'analyse sans broncher. Mais la section 8.1 exige que le JSON échangé entre systèmes soit en UTF-8, et la RFC 3629, section 3, interdit à UTF-8 d'encoder quoi que ce soit entre U+D800 et U+DFFF. Mesuré sur cet outil : le texte {"k":"id-�-end"} s'analyse, et l'encodage du résultat en UTF-8 donne les octets 69 64 2d ef bf bd 2d 65 6e 64 — la moitié isolée est devenue U+FFFD, le caractère de remplacement, et la valeur ne fait plus l'aller-retour. Rien n'a levé d'exception. C'est cette panne-là qu'il faut chercher.
La RFC 8259 n'impose l'échappement que de trois choses dans une chaîne JSON. Tout le reste est facultatif. Celle qui casse réellement les chaînes de traitement est le demi-surrogate isolé : légal dans un texte JSON, impossible en UTF-8, et remplacé en silence dès que tes données sont écrites.
Trois obligatoires, tout le reste facultatif
La règle est plus courte que la plupart ne l'imaginent. Tous les caractères Unicode peuvent figurer entre les guillemets, sauf ceux qui doivent être échappés : le guillemet lui-même, l'antislash et les caractères de contrôle de U+0000 à U+001F. Trois éléments. Un chemin Windows plein d'antislashs et de guillemets, C:\Users\Léa\"rapport".txt, réclame chaque antislash doublé et chaque guillemet échappé, et le é reste exactement tel quel. Aucune règle sur les accents, aucune sur les écritures non latines, aucune sur les emoji.
Deux limites méritent d'être vérifiées plutôt que devinées. Le caractère DEL en U+007F n'est pas un caractère de contrôle au sens de cette règle — la plage s'arrête à U+001F — il reste donc littéral, et le passage dans l'outil le confirme : la sortie est le caractère lui-même, un octet en UTF-8, sans échappement. Le séparateur de ligne U+2028 et le séparateur de paragraphe U+2029 sont laissés de même, car ils ne sont pas non plus dans la plage U+0000 à U+001F. C'était jadis un danger réel quand on collait du JSON dans un script en ligne, un littéral de chaîne JavaScript ne pouvant pas les contenir ; le langage a été modifié en 2019 pour l'autoriser. Si ta sortie doit encore survivre à un vieil analyseur, le mode tout-ASCII de l'outil les échappe en \u2028 et \u2029, comme tout ce qui dépasse la plage ASCII.
La barre oblique échappée mérite un paragraphe, car c'est la chose la plus courante que l'on fait sans savoir pourquoi. JSON permet \/ et ne l'exige jamais. L'habitude vient de l'insertion de JSON dans un élément script HTML, où un </script> littéral dans une chaîne fermerait l'élément trop tôt et livrerait le reste de tes données à l'analyseur HTML ; échapper la barre coupe la séquence et le navigateur ne la voit jamais. L'outil a un interrupteur pour cela, désactivé par défaut, ce qui est le bon réglage : active-le quand tu incorpores, laisse-le coupé partout ailleurs. Échapper les barres dans une réponse d'API grossit la charge utile sans changer une valeur.
Des unités UTF-16, pas des caractères
L'échappement \uXXXX porte exactement quatre chiffres hexadécimaux, soit une unité UTF-16, ce qui couvre U+0000 à U+FFFF. Tout ce qui est au-dessus doit s'écrire avec deux d'entre eux, et la spécification est explicite : pour échapper un caractère étendu hors du plan multilingue de base, il est représenté par une séquence de douze caractères encodant la paire de surrogates UTF-16. Il n'existe pas de forme à six chiffres. Passe un visage souriant dans l'outil en mode ASCII et il produit 😀, ce qui est juste ; si tu vois un jour \u1f600 dans la sortie de quelqu'un, ce n'est pas du JSON, c'est une autre convention d'échappement venue de Python ou d'un shell.
C'est pourquoi l'outil affiche quatre compteurs pour la même chaîne et pourquoi ils divergent. Un visage souriant fait un point de code, deux unités UTF-16 et quatre octets UTF-8. Un emoji de famille composé de quatre personnages reliés par des liants de largeur nulle fait sept points de code, onze unités UTF-16 et vingt-cinq octets UTF-8. Si ta colonne de base de données est déclarée à vingt caractères, lequel de ces trois nombres cela désigne est une propriété de la base et non de tes données, et c'est dans l'écart entre eux que vivent les erreurs de troncature.
Le demi-surrogate isolé, et comment il arrive dans tes données
Une paire de surrogates, ce sont deux unités de code qui ne signifient quelque chose qu'ensemble : une moitié haute de U+D800 à U+DBFF suivie d'une moitié basse de U+DC00 à U+DFFF. La grammaire JSON n'impose pas l'appariement. La section 8.2 de la RFC 8259 le dit en toutes lettres, en notant que la spécification permet aux valeurs de chaîne de contenir des suites de bits qui ne peuvent pas encoder de caractères Unicode, et en donnant � pour exemple. Ainsi {"k":"id-�-end"} est un texte JSON syntaxiquement valide. Tous les analyseurs que tu rencontreras l'acceptent.
La contradiction arrive une section plus tôt. La section 8.1 exige que le texte JSON échangé entre systèmes n'appartenant pas à un écosystème fermé soit encodé en UTF-8, et la RFC 3629, qui définit UTF-8, interdit d'encoder les numéros de caractère entre U+D800 et U+DFFF, précisément parce qu'ils sont réservés à UTF-16. Une valeur que JSON autorise ne peut donc pas s'exprimer dans l'encodage que JSON impose. Ce que font les implémentations au lieu d'échouer, c'est substituer : passe ce texte JSON dans un analyseur et ré-encode le résultat, les octets reviennent en 69 64 2d ef bf bd 2d 65 6e 64. Ces trois octets du milieu, EF BF BD, sont U+FFFD, le caractère de remplacement. La valeur entrée n'est pas la valeur sortie, et rien nulle part n'a levé d'erreur.
Personne ne tape un demi-surrogate. Ils viennent de la troncature. Prends la chaîne Rapport 📊 final, seize unités UTF-16 pour quinze points de code, et coupe-la à neuf unités pour tenir dans une étiquette : le résultat est Rapport suivi d'un demi-emoji, que l'outil échappe en Rapport �. C'est l'origine quotidienne : une colonne de base de données limitée en caractères, une interface qui rogne un titre, une ligne de journal coupée à une largeur fixe, un import qui recopie des champs de longueur fixe. Partout où une chaîne est coupée en comptant des unités plutôt que des points de code, la coupe peut tomber au milieu d'une paire.
L'outil te donne deux moyens d'en repérer un avant qu'il ne voyage. L'échappeur écrit toujours un demi-surrogate isolé sous forme \uXXXX, même dans son mode par défaut non-ASCII — exactement ce que fait un JSON.stringify moderne — un � ou un � inattendu dans la sortie est donc le signal. Et le compteur d'octets, qui donne les mêmes nombres qu'un vrai encodeur UTF-8, aura déjà compté cette moitié pour trois octets : trois octets, c'est le prix de U+FFFD, si bien qu'une chaîne dont la forme échappée contient un demi-surrogate est déjà facturée pour un caractère de remplacement avant même d'avoir quitté ton écran.
Là où cet outil est volontairement plus permissif qu'un analyseur
Dans le sens de l'échappement, la sortie par défaut de l'outil correspond, caractère pour caractère, à ce que produit un JSON.stringify moderne, sur tous les cas essayés : caractères de contrôle, emoji, accents combinants, chemins Windows, demi-surrogates isolés. Si tu n'utilises que les réglages par défaut, le résultat est le contenu d'une chaîne JSON sans les guillemets extérieurs, et rien de plus surprenant.
Dans le sens du déséchappement, il est volontairement plus souple, et savoir où compte. Un vrai analyseur JSON rejette un caractère de contrôle brut dans une chaîne : colle un caractère de sonnerie littéral dans un document JSON et tu obtiens une erreur de syntaxe qui le dit. Cet outil l'accepte et t'le rend, au motif que tu inspectes un fragment plutôt que tu ne valides un document. Passer une chaîne dans le mode déséchappement sans protestation ne prouve donc pas que le JSON qui l'entoure est valide. Il rejette néanmoins les trois choses qui rendent un fragment absurde : un échappement inconnu comme \x, un \u incomplet ou non hexadécimal, et un antislash isolé tout à la fin — en signalant à chaque fois la position et la séquence fautive.
L'interrupteur tout-ASCII est l'autre réglage à comprendre, car il change la taille de ta charge utile, pas son sens. Échapper tout ce qui dépasse la plage ASCII rend la sortie transportable à travers un système à l'encodage incertain, à un coût réel : Café passe de quatre caractères à neuf, et un emoji seul de deux à douze. Utilise-le quand le transport est douteux — un vieil agrégateur de journaux, une URL, un en-tête, une base dont tu ne connais pas la collation — et pas autrement, car UTF-8 est ce que demande la section 8.1 et il est plus compact.
| Entrée | Exigé par la RFC 8259 ? | Sortie de l'outil (mode par défaut) |
|---|---|---|
| Un guillemet droit | Oui — obligatoire | Échappement de deux caractères |
| Un antislash | Oui — obligatoire | Doublé |
| Une tabulation, U+0009 | Oui — obligatoire (caractère de contrôle) | La forme courte de la tabulation, pas un échappement de six caractères |
| Le caractère de sonnerie, U+0007 | Oui — obligatoire (contrôle sans forme courte) | Un échappement de six caractères se terminant par 0007 |
| DEL, U+007F | Non — la plage de contrôle s'arrête à U+001F | Laissé littéral, un octet UTF-8 |
| Une barre oblique | Non — peut être échappée, jamais obligatoire | Laissée telle quelle, sauf si l'option barre est activée |
| Un emoji hors du plan de base | Non — l'UTF-8 littéral convient | Conservé tel quel ; en mode ASCII, une paire de surrogates de douze caractères |
| Un demi-surrogate isolé | Représentable, mais pas encodable en UTF-8 | Toujours échappé en \uXXXX, même en mode par défaut — c'est le signal d'alarme |
Questions fréquentes
- Quels caractères dois-je échapper dans une chaîne JSON ?
- Exactement trois catégories, selon la RFC 8259, section 7 : le guillemet droit, l'antislash et tout caractère de contrôle de U+0000 à U+001F. Rien d'autre n'est obligatoire. Lettres accentuées, idéogrammes, emoji, DEL en U+007F et les séparateurs U+2028 et U+2029 peuvent tous figurer en UTF-8 littéral dans la chaîne. Parmi les caractères de contrôle, cinq ont une forme courte — retour arrière, saut de page, saut de ligne, retour chariot et tabulation — et les autres exigent la forme \u à six caractères, ainsi qu'on écrit une sonnerie ou un octet nul. N'importe quoi peut être échappé si tu le veux, d'où ce JSON parfaitement valide qui arrive parfois avec chaque caractère non-ASCII épelé : il est plus gros, pas plus correct.
- Comment écrire un emoji dans une chaîne JSON ?
- Deux façons, toutes deux correctes. Le laisser en UTF-8 littéral : rien dans la spécification n'exige de l'échapper, et c'est ce que fait l'outil par défaut. Ou, si la charge utile doit être en ASCII, écrire la paire de surrogates : deux échappements \uXXXX, douze caractères en tout, un pour la moitié haute et un pour la moitié basse. Il n'existe pas de forme à six chiffres : \u1f600 n'est pas du JSON, et un analyseur qui l'accepte ne fait pas ce que dit la norme. Le coût de la forme ASCII est réel — un emoji passe de deux caractères à douze — ne l'utilise donc que si l'on ne peut pas confier de l'UTF-8 au transport.
- Qu'est-ce qu'un demi-surrogate isolé et pourquoi casse-t-il tout ?
- C'est la moitié d'un encodage à deux unités, sans partenaire : une unité de code entre U+D800 et U+DFFF toute seule. JSON l'autorise — la RFC 8259, section 8.2, reconnaît explicitement que les valeurs de chaîne peuvent contenir des suites de bits incapables d'encoder des caractères Unicode, et donne un surrogate non apparié en exemple — mais UTF-8 ne peut pas l'exprimer, la RFC 3629 interdisant d'encoder quoi que ce soit dans cette plage. Comme la section 8.1 exige l'UTF-8 pour l'échange, les deux règles se heurtent, et ce qui se produit en pratique est une substitution : la moitié devient U+FFFD, le caractère de remplacement, trois octets, et la valeur d'origine a disparu. Aucune exception n'est levée en chemin, d'où le fait que cela ressorte des jours plus tard sous la forme d'une recherche qui ne trouve plus rien ou d'un identifiant qui ne se joint plus.
- Faut-il échapper la barre oblique ?
- Uniquement quand tu incorpores le JSON dans un élément script HTML. JSON autorise \/ et ne l'exige jamais ; l'échappement existe parce qu'un </script> littéral dans une valeur de chaîne fermerait l'élément trop tôt et livrerait le reste de tes données à l'analyseur HTML comme du balisage. Casser la séquence avec un antislash l'empêche. Partout ailleurs — réponse d'API, fichier sur disque, message dans une file — échapper les barres ne fait qu'enfler la charge utile. L'outil livre l'option désactivée, ce qui est le bon défaut, et l'activer ne change rien au sens de la valeur une fois analysée.
- L'outil a accepté ma chaîne mais mon analyseur la refuse. Pourquoi ?
- Parce que le sens déséchappement est volontairement plus tolérant qu'un analyseur de document, et la différence porte sur les caractères de contrôle. Un analyseur JSON refuse un caractère de contrôle brut posé littéralement dans une chaîne — le message dit en général « bad control character in string literal » — alors que cet outil l'accepte et t'le rend, en supposant que tu inspectes un fragment plutôt que tu ne valides un document. Si ton analyseur proteste et pas l'outil, cherche une tabulation ou un saut de ligne brut qui aurait dû être écrit en échappement. L'outil rejette néanmoins les trois défauts qui rendent un fragment illisible : une séquence d'échappement inconnue, un \u incomplet ou non hexadécimal, et un antislash isolé en fin de chaîne — en te donnant la position de chacun.
Articles qui pourraient t'intéresser
Tous les guides →Outils similaires
Ceci décrit le comportement d'un format de fichier et d'un moteur de rendu, vérifié sur la spécification citée et sur le code de l'outil tel qu'il existe aujourd'hui. Les moteurs de rendu divergent : GitHub, GitLab, un générateur de site statique et l'aperçu de ton éditeur sont quatre implémentations différentes, et ce qui passe dans l'une peut échouer dans l'autre. Rien ici n'est une garantie sur ta chaîne de publication — teste le résultat là où il sera réellement publié, et considère tout outil, celui-ci compris, comme quelque chose à vérifier et non à croire.
Sources
- RFC Editor — RFC 8259, The JavaScript Object Notation (JSON) Data Interchange Format, December 2017, Internet Standard STD 90 — section 7 lists the characters that MUST be escaped (quotation mark, reverse solidus, U+0000 to U+001F), states that any character MAY be escaped, and defines the \uXXXX form and the twelve-character surrogate pair; section 8.1 requires UTF-8 for interchange; section 8.2 acknowledges that the grammar permits an unpaired surrogate such as �
- RFC Editor — RFC 3629, UTF-8, a transformation format of ISO 10646, November 2003 — section 3 prohibits UTF-8 from encoding character numbers between U+D800 and U+DFFF because they are reserved for use with the UTF-16 encoding form
- Ecma International — ECMA-404, The JSON Data Interchange Syntax, 2nd edition, December 2017 — the syntax half of the same standard, published in parallel with RFC 8259 and freely downloadable
- Ecma International — ECMAScript Language Specification, JSON.stringify — since the well-formed JSON.stringify change of ES2019, a lone surrogate in a JavaScript string is emitted as a \uXXXX escape rather than as an unpaired code unit, which is the behaviour this tool reproduces
Tu as repéré une erreur dans cet article ?