GoSuda

Analyse des modifications de comportement de encoding/json/v2 dans Go 1.25 et de la couche de compatibilité avec la v1

By lemonmint views ...

Le package encoding/json v2 de Go est une nouvelle implémentation destinée à remédier à plusieurs inconvénients de la v1 existante (manque de cohérence, comportements surprenants, problèmes de performance). Il s'agit d'une fonctionnalité expérimentale, activée par la balise de compilation goexperiment.jsonv2.

Le point le plus important est que lorsque la v2 est activée, la v1 fonctionne comme une couche de compatibilité qui émule le comportement de la v1 au-dessus de l'implémentation v2. Ce mécanisme passe par la fonction DefaultOptionsV1() du fichier v2_v1.go. Autrement dit, la v2 fournit des options permettant de reproduire fidèlement le comportement de la v1, tout en proposant de nouveaux comportements par défaut plus stricts et plus prévisibles.

Les principaux objectifs de la v2 sont les suivants.

  1. Amélioration de l'exactitude et de la prévisibilité : en appliquant par défaut des règles plus strictes (par exemple, la distinction entre majuscules et minuscules, l'interdiction des clés en double), elle réduit les comportements inattendus.
  2. Amélioration des performances : le moteur d'analyse et d'encodage a été repensé afin d'accroître l'efficacité.
  3. Extension de la flexibilité et du contrôle : un système d'options détaillé est introduit afin que les développeurs puissent contrôler finement le traitement JSON.

Principales différences de sémantique et de comportement

Les différences de comportement entre la v1 et la v2 sont résumées par point, en se fondant principalement sur le fichier v2_test.go.

1. Correspondance des noms de champs (sensibilité à la casse)

  • Comportement de la v1 : lors du démarshaling d'un membre d'objet JSON vers un champ de structure Go, la correspondance ignore la casse (insensible à la casse). "FirstName" et "firstname" sont tous deux associés au champ FirstName.
  • Comportement de la v2 : par défaut, la correspondance respecte la casse (sensible à la casse) et n'associe que les champs dont le nom correspond exactement.
  • Raison du changement : la correspondance insensible à la casse peut être à l'origine de comportements inattendus et entraîne une dégradation des performances lors du traitement des champs non correspondants. La v2 adopte par défaut un comportement plus clair et plus prévisible.
  • Options associées : en v2, il est possible d'activer explicitement l'ignorance de la casse champ par champ à l'aide de l'option de balise json:"...,case:ignore", ou de l'appliquer globalement avec json.MatchCaseInsensitiveNames(true).

2. Modification de la sémantique de l'option de balise omitempty

  • Comportement de la v1 : un champ est omis en fonction de l'« état vide » de la valeur Go. Ici, l'« état vide » désigne false, 0, les pointeurs ou interfaces nil, ainsi que les tableaux, slices, maps et chaînes de longueur nulle.
  • Comportement de la v2 : un champ est omis en fonction de l'« état vide » de la valeur JSON encodée. Autrement dit, il est omis lorsqu'il est encodé en null, "", {} ou [].
  • Raison du changement : la définition de la v1 dépend du système de types de Go. La v2 se fonde sur le système de types JSON afin d'offrir un comportement plus cohérent. Par exemple, en v1, la valeur false d'un champ de type bool est omise, alors qu'en v2, false est une valeur JSON non vide et n'est donc pas omise. La v2 introduit l'option omitzero, qui permet de remplacer le comportement de l'omitempty de la v1 appliqué à 0 ou à false.
  • Options associées : pour obtenir en v2 le comportement identique à la v1, on utilise l'option json.OmitEmptyWithLegacyDefinition(true).

3. Modification du comportement de l'option de balise string

  • Comportement de la v1 : s'applique aux champs de types numériques, booléens et chaînes. La valeur correspondante est réencodée à l'intérieur d'une chaîne JSON (par exemple, int(42) devient "42"). Elle ne s'applique pas de manière récursive aux valeurs contenues dans les types composites (slices, maps, etc.).
  • Comportement de la v2 : s'applique uniquement aux types numériques et de manière récursive. Ainsi, les nombres contenus dans un slice tel que []int sont également encodés en chaînes JSON.
  • Raison du changement : l'usage principal de l'option string est de représenter les entiers de 64 bits sous forme de chaînes afin d'éviter la perte de précision. Le comportement de la v1 était limité et peu cohérent. La v2 se concentre sur cet usage essentiel et étend le comportement de manière récursive afin de le rendre plus utile.
  • Options associées : l'option json.StringifyWithLegacySemantics(true) permet d'imiter le comportement de la v1.

4. Marshaling des slices et maps nil

  • Comportement de la v1 : les slices nil et les maps nil sont marshalés en null.
  • Comportement de la v2 : par défaut, les slices nil sont marshalés en [] (tableau vide) et les maps nil en {} (objet vide).
  • Raison du changement : nil est un détail d'implémentation du langage Go, et il n'est pas souhaitable de l'exposer dans un format JSON indépendant du langage. Les notations [] et {}, qui représentent des collections vides, sont des représentations plus universelles.
  • Options associées : en v2, les options json.FormatNilSliceAsNull(true) ou json.FormatNilMapAsNull(true) permettent de marshaler en null comme dans la v1.

5. Démarshaling des tableaux

  • Comportement de la v1 : lors du démarshaling vers un tableau Go ([N]T), aucune erreur n'est levée si la longueur du tableau JSON diffère de celle du tableau Go. Si le tableau JSON est plus court, les éléments restants sont remplis avec la valeur zéro ; s'il est plus long, l'excédent est ignoré.
  • Comportement de la v2 : la longueur du tableau JSON doit correspondre exactement à celle du tableau Go. Dans le cas contraire, une erreur est levée.
  • Raison du changement : en Go, la longueur d'un tableau de taille fixe revêt souvent une signification importante. Le comportement de la v1 peut entraîner une perte silencieuse de données. La v2 adopte des règles plus strictes afin d'améliorer l'exactitude.
  • Options associées : l'option json.UnmarshalArrayFromAnyLength(true) permet d'imiter le comportement de la v1.

6. Traitement de time.Duration

  • Comportement de la v1 : time.Duration est traité en interne comme un int64 et encodé sous forme de nombre JSON exprimé en nanosecondes.
  • Comportement de la v2 : time.Duration est encodé sous forme de chaîne JSON au format "1h2m3s", à l'aide de la méthode time.Duration.String().
  • Raison du changement : les nombres exprimés en nanosecondes sont peu lisibles, et la représentation textuelle standard de time.Duration est plus utile.
  • Options associées : il est possible de retrouver le comportement de la v1 grâce à l'option de balise json:",format:nano" ou à l'option json.FormatTimeWithLegacySemantics(true).

7. Traitement de l'UTF-8 invalide

  • Comportement de la v1 : lors du marshaling ou du démarshaling, les octets UTF-8 invalides présents dans une chaîne sont remplacés silencieusement par le caractère de remplacement Unicode (\uFFFD).
  • Comportement de la v2 : par défaut, une erreur est renvoyée lorsqu'un UTF-8 invalide est rencontré.
  • Raison du changement : il s'agit d'éviter la corruption silencieuse des données et de se conformer à la norme JSON plus stricte (RFC 7493).
  • Options associées : l'option jsontext.AllowInvalidUTF8(true) permet d'imiter le comportement de la v1.

8. Traitement des noms de membres d'objet en double

  • Comportement de la v1 : la présence de membres portant le même nom au sein d'un objet JSON est autorisée. La dernière valeur rencontrée remplace les précédentes.
  • Comportement de la v2 : par défaut, la présence de noms de membres en double entraîne une erreur.
  • Raison du changement : la norme RFC 8259 ne définit pas le comportement en cas de noms dupliqués, ce qui peut conduire à des comportements différents selon les implémentations. Cela peut être à l'origine de vulnérabilités de sécurité. La v2 rejette explicitement ces cas afin d'améliorer l'exactitude et la sécurité.
  • Options associées : l'option jsontext.AllowDuplicateNames(true) permet d'imiter le comportement de la v1.

Différences d'implémentation et d'architecture

  • v1 : repose largement sur decodeState dans decode.go et sur une machine à états écrite manuellement dans scanner.go. Il s'agit d'une structure monolithique dans laquelle l'analyse syntaxique et l'analyse sémantique sont fortement couplées.
  • v2 : l'architecture est plus modulaire.
    • encoding/json/jsontext : fournit un tokenizer JSON (Decoder) et un encodeur (Encoder) de bas niveau et hautes performances. Ce package se concentre uniquement sur l'aspect syntaxique de JSON.
    • encoding/json/v2 : s'appuie sur jsontext pour gérer la conversion sémantique entre les types Go et les valeurs JSON.
    • Cette séparation dissocie l'analyse syntaxique de l'analyse sémantique, ce qui améliore la clarté du code et ses performances.

Nouvelles API et fonctionnalités de la v2

La v2 offre un contrôle très souple grâce au système json.Options.

  • json.Options : ensemble d'options modifiant le comportement du marshaling et du démarshaling.
  • json.JoinOptions(...) : fusionne plusieurs options en une seule.
  • WithMarshalers / WithUnmarshalers : fonctionnalité puissante permettant d'injecter une logique de sérialisation et de désérialisation pour un type donné, sans que ce type n'implémente les interfaces Marshaler ou Unmarshaler. Elle est particulièrement utile pour traiter des types provenant de packages externes.
  • Nouvelles options : RejectUnknownMembers, Deterministic(false), FormatNilSliceAsNull, etc., qui permettent un contrôle du comportement impossible en v1.

Conclusion

encoding/json v2 est une implémentation moderne qui améliore nettement l'exactitude, les performances et la flexibilité à partir de l'expérience acquise avec la v1. Le comportement par défaut est devenu plus strict, mais le système Options permet de reproduire intégralement tous les comportements de la v1. Il est ainsi possible de préserver la compatibilité avec le code existant tout en adoptant progressivement les avantages de la v2.

  • Pour un nouveau projet, il est recommandé d'utiliser la v2 par défaut.
  • Pour un projet existant, on peut continuer à utiliser jsonv1 tel quel, ou migrer vers jsonv2 en introduisant progressivement le comportement strict de la v2 via DefaultOptionsV1().