GoSuda

Comparaison de `encoding/json` v1 vs v2 dans Go 1.25

By lemonmint
views ...

Le package encoding/json v2 de Go est une nouvelle implémentation visant à améliorer plusieurs lacunes de la v1 existante (manque de cohérence, comportements surprenants, problèmes de performance). Il s'agit d'une fonctionnalité expérimentale activée via le tag de build 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 de la v2. Ceci est réalisé via la fonction DefaultOptionsV1() dans le fichier v2_v1.go. En d'autres termes, la v2 offre des options pour reproduire parfaitement le comportement de la v1, tout en présentant de nouveaux comportements par défaut plus stricts et prévisibles.

Les objectifs principaux de la v2 sont les suivants :

  1. Amélioration de la précision et de la prévisibilité : Appliquer par défaut des règles plus strictes (par exemple, sensible à la casse, interdiction des clés dupliquées) pour réduire les comportements inattendus.
  2. Amélioration des performances : Refonte des moteurs d'analyse et d'encodage pour accroître l'efficacité.
  3. Accroissement de la flexibilité et du contrôle : Introduction d'un système d'options détaillé permettant aux développeurs de contrôler finement la manière dont le JSON est traité.

Différences majeures de sémantique/comportement

Les différences de comportement entre la v1 et la v2 sont organisées par élément, en se concentrant sur le fichier v2_test.go.

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

  • Comportement de la v1 : Lors de la désérialisation (unmarshaling) des membres d'objets JSON vers des champs de structures Go, la correspondance est effectuée sans tenir compte de la casse (case-insensitive). Aussi bien "FirstName" que "firstname" sont mappés au champ FirstName.
  • Comportement de la v2 : Par défaut, la correspondance est sensible à la casse (case-sensitive), et seuls les champs correspondant exactement sont mappés.
  • Raison du changement : La correspondance insensible à la casse peut être une source de comportements inattendus et entraîner une dégradation des performances lors du traitement des champs non correspondants. La v2 a adopté un comportement par défaut plus clair et prévisible.
  • Option associée : En v2, l'option de tag json:"...,case:ignore" peut être utilisée pour activer explicitement l'insensibilité à la casse par champ, ou l'option json.MatchCaseInsensitiveNames(true) peut être appliquée globalement.

2. Changement de la signification de l'option de tag omitempty

  • Comportement de la v1 : Omet le champ en fonction de l'état "vide" de la valeur Go. Ici, "vide" signifie false, 0, les pointeurs/interfaces nil, et les tableaux/slices/maps/chaînes de longueur nulle.
  • Comportement de la v2 : Omet le champ en fonction de l'état "vide" de la valeur JSON encodée. C'est-à-dire qu'il est omis s'il est encodé en null, "", {}, [].
  • Raison du changement : La définition de la v1 dépend du système de types de Go. La v2 fournit un comportement plus cohérent en se basant sur le système de types JSON. Par exemple, en v1, une valeur false de type bool est omise, mais en v2, false n'est pas une valeur JSON vide et n'est donc pas omise. La v2 ajoute l'option omitzero pour remplacer le comportement de omitempty de la v1 qui s'appliquait à 0 ou false.
  • Option associée : Si le même comportement que la v1 est souhaité en v2, utilisez l'option json.OmitEmptyWithLegacyDefinition(true).

3. Changement de comportement de l'option de tag string

  • Comportement de la v1 : S'applique aux champs de type numérique, booléen et chaîne de caractères. Réencode la valeur à l'intérieur d'une chaîne JSON (par exemple, int(42) -> "42"). Ne s'applique pas récursivement aux valeurs à l'intérieur de types composites (slices, maps, etc.).
  • Comportement de la v2 : S'applique uniquement aux types numériques et est appliqué récursivement. C'est-à-dire que tous les nombres à l'intérieur d'un slice comme []int sont également encodés en chaînes JSON.
  • Raison du changement : L'utilisation principale de l'option string est de représenter les nombres sous forme de chaînes pour éviter la perte de précision des entiers 64 bits. Le comportement de la v1 était limité et manquait de cohérence. La v2 se concentre sur cette utilisation essentielle et étend le comportement de manière récursive pour le rendre plus utile.
  • Option associée : L'option json.StringifyWithLegacySemantics(true) peut être utilisée pour imiter le comportement de la v1.

4. Marshalling de slices et maps nil

  • Comportement de la v1 : Les slices nil et les maps nil sont marshallés en null.
  • Comportement de la v2 : Par défaut, les slices nil sont marshallés en [] (tableau vide), et les maps nil sont marshallés 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 au format JSON indépendant du langage. Les expressions [] ou {} pour représenter des collections vides sont plus universelles.
  • Option associée : En v2, les options json.FormatNilSliceAsNull(true) ou json.FormatNilMapAsNull(true) peuvent être utilisées pour marshaller en null comme en v1.

5. Désérialisation (Unmarshaling) de tableaux

  • Comportement de la v1 : Lors de la désérialisation vers un tableau Go ([N]T), aucune erreur n'est générée même si la longueur du tableau JSON diffère de la longueur du tableau Go. Si la longueur est plus courte, les espaces restants sont remplis de valeurs zéro ; si la longueur est plus longue, l'excédent est ignoré.
  • Comportement de la v2 : La longueur du tableau JSON doit correspondre exactement à la longueur du tableau Go. Sinon, une erreur est générée.
  • Raison du changement : Dans Go, la longueur d'un tableau de taille fixe a souvent une signification importante. Le comportement de la v1 pouvait entraîner une perte silencieuse de données. La v2 a augmenté la précision avec des règles plus strictes.
  • Option associée : L'option json.UnmarshalArrayFromAnyLength(true) peut être utilisée pour 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é en un nombre JSON représentant des nanosecondes.
  • Comportement de la v2 : Est encodé en une chaîne JSON au format "1h2m3s" en utilisant la méthode time.Duration.String().
  • Raison du changement : Les nanosecondes numériques sont moins lisibles, et la représentation standard des chaînes de caractères de time.Duration est plus utile.
  • Option associée : L'option de tag json:",format:nano" ou l'option json.FormatTimeWithLegacySemantics(true) peuvent être utilisées pour obtenir le comportement de la v1.

7. Traitement des UTF-8 invalides

  • Comportement de la v1 : Lors du marshalling/unmarshalling, si des octets UTF-8 invalides sont présents dans une chaîne, ils sont silencieusement remplacés par le caractère de remplacement Unicode (\uFFFD).
  • Comportement de la v2 : Par défaut, une erreur est renvoyée si un UTF-8 invalide est rencontré.
  • Raison du changement : Pour prévenir la corruption silencieuse des données et pour se conformer à la norme JSON plus stricte (RFC 7493).
  • Option associée : L'option jsontext.AllowInvalidUTF8(true) peut être utilisée pour imiter le comportement de la v1.

8. Traitement des noms de membres d'objets dupliqués

  • Comportement de la v1 : Autorise la présence de membres avec le même nom dupliqués dans un objet JSON. La dernière valeur rencontrée écrase les précédentes.
  • Comportement de la v2 : Par défaut, une erreur est renvoyée si des noms de membres dupliqués sont présents.
  • Raison du changement : La norme RFC 8259 ne définit pas le comportement des noms dupliqués, ce qui peut entraîner des comportements différents selon les implémentations. Cela peut être une source de vulnérabilités de sécurité. La v2 rejette explicitement cela pour augmenter la précision et la sécurité.
  • Option associée : L'option jsontext.AllowDuplicateNames(true) peut être utilisée pour imiter le comportement de la v1.

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

  • v1 : Dépend fortement de decodeState dans decode.go et de la machine à états écrite manuellement dans scanner.go. Il s'agit d'une structure monolithique où la logique d'analyse et l'analyse sémantique sont fortement couplées.
  • v2 : L'architecture est plus modularisée.
    • encoding/json/jsontext : Fournit un tokenizeur (Decoder) et un encodeur (Encoder) JSON de bas niveau et haute performance. Ce package se concentre uniquement sur l'aspect syntaxique du JSON.
    • encoding/json/v2 : Gère la conversion sémantique entre les types Go et les valeurs JSON, en se basant sur jsontext.
    • Cette séparation a permis de distinguer l'analyse syntaxique de l'analyse sémantique, améliorant ainsi la clarté du code et les performances.

Nouvelles API et fonctionnalités de la v2

La v2 offre des capacités de contrôle très flexibles grâce au système json.Options.

  • json.Options : Un ensemble d'options qui modifient le comportement de marshalling/unmarshalling.
  • json.JoinOptions(...) : Fusionne plusieurs options en une seule.
  • WithMarshalers / WithUnmarshalers : Une fonctionnalité puissante qui permet d'injecter une logique de sérialisation/désérialisation pour des types spécifiques sans avoir à implémenter les interfaces Marshaler/Unmarshaler. Ceci est particulièrement utile lors du traitement de types provenant de packages externes.
  • Nouvelles options : RejectUnknownMembers, Deterministic(false), FormatNilSliceAsNull, etc. Une variété de contrôles de comportement qui n'étaient pas possibles en v1 sont désormais disponibles.

Conclusion

encoding/json v2 est une implémentation moderne qui améliore considérablement la précision, les performances et la flexibilité en se basant sur l'expérience de la v1. Bien que le comportement par défaut soit plus strict, le système Options sophistiqué prend en charge parfaitement tous les comportements de la v1, permettant d'introduire progressivement les avantages de la v2 tout en maintenant la compatibilité avec le code existant.

  • Pour les nouveaux projets, il est recommandé d'utiliser la v2 par défaut.
  • Les projets existants peuvent continuer à utiliser jsonv1, ou migrer vers jsonv2 en utilisant DefaultOptionsV1() pour introduire progressivement les comportements plus stricts de la v2.