Sérialisation JSON avec Golang

Vue d'ensemble

JSON est l’un des formats de sérialisation les plus populaires. Il est lisible par l'homme, raisonnablement concis et peut être analysé facilement par toute application Web utilisant JavaScript. Go, en tant que langage de programmation moderne, offre un support de premier ordre pour la sérialisation JSON dans sa bibliothèque standard. 

Mais il y a quelques coins et recoins. Dans ce didacticiel, vous apprendrez à sérialiser et à désérialiser efficacement des données arbitraires et structurées vers / depuis JSON. Vous apprendrez également à gérer des scénarios avancés tels que les énumérations de sérialisation..

Le forfait json

Go prend en charge plusieurs formats de sérialisation dans le package de codage de sa bibliothèque standard. L'un d'entre eux est le format JSON populaire. Vous sérialisez les valeurs de Golang à l'aide de la fonction Marshal () en une tranche d'octets. Vous désérialisez une tranche d'octets en une valeur de Golang à l'aide de la fonction Unmarshal (). C'est si simple. Les termes suivants sont équivalents dans le contexte de cet article:

  • Sérialisation / Encodage / Marshalling
  • Deserialization / Decoding / Unmarshalling

Je préfère la sérialisation car elle reflète le fait que vous convertissez une structure de données potentiellement hiérarchique vers / depuis un flux d'octets..

Maréchal

La fonction Marshal () peut tout prendre, ce qui, dans Go, désigne l'interface vide et renvoie une tranche d'octets et d'erreur. Voici la signature:

func Marshal (v interface ) ([] octet, erreur)

Si Marshal () ne réussit pas à sérialiser la valeur d'entrée, une erreur non nulle sera renvoyée. Marshal () a des limitations strictes (nous verrons plus loin comment les surmonter avec des marshallers personnalisés):

  • Les clés de la carte doivent être des chaînes.
  • Les valeurs de la carte doivent être des types sérialisables par le package json.
  • Les types suivants ne sont pas pris en charge: canal, complexe et fonction.
  • Les structures de données cycliques ne sont pas supportées.
  • Les pointeurs seront codés (et décodés plus tard) comme les valeurs vers lesquelles ils pointent (ou 'null' si le pointeur est nil).

Unmarshal

La fonction Unmarshal () prend une tranche d'octet qui, espérons-le, représente un code JSON valide et une interface de destination, qui est généralement un pointeur sur un type struct ou basic. Il désérialise le JSON dans l'interface de manière générique. Si la sérialisation a échoué, une erreur sera renvoyée. Voici la signature:

func Unmarshal (erreur [data [] octet, v interface )

Sérialisation de types simples

Vous pouvez facilement sérialiser des types simples comme utiliser le package json. Le résultat ne sera pas un objet JSON à part entière, mais une simple chaîne. Ici, l'int 5 est sérialisé dans le tableau d'octets [53], ce qui correspond à la chaîne "5"..

 // Serialize int var x = 5 octets, err: = json.Marshal (x) si err! = Nil fmt.Println ("Impossible de sérislize", x) fmt.Printf ("% v =>% v , '% v' \ n ", x, octets, chaîne (octets)) // Désérialisation int var erreur int = json.Unmarshal (octets, & r) si err! = nil fmt.Println (" Impossible de désérisliser ", octets) fmt.Printf ("% v =>% v \ n ", octets, r) Sortie: - 5 => [53], '5' - [53] => 5

Si vous essayez de sérialiser des types non pris en charge comme une fonction, vous obtiendrez une erreur:

 // Essayer de sérialiser une fonction foo: = func () fmt.Println ("foo () ici") octets, err = json.Marshal (foo) si err! = Nil fmt.Println (err) Sortie : json: type non supporté: func ()

Sérialisation de données arbitraires avec des cartes

La puissance de JSON réside dans le fait qu’il peut très bien représenter des données hiérarchiques arbitraires. Le package JSON le prend en charge et utilise l'interface générique vide (interface ) pour représenter toute hiérarchie JSON. Voici un exemple de désérialisation et de sérialisation ultérieure d'une arborescence binaire où chaque nœud a une valeur int et deux branches, gauche et droite, pouvant contenir un autre nœud ou être null.

La valeur JSON nulle est équivalente à la valeur Go Nil. Comme vous pouvez le voir dans la sortie, le json.Unmarshal () function a converti avec succès le blob JSON en une structure de données Go constituée d’une carte imbriquée d’interfaces et a préservé le type de valeur int. le json.Marshal () fonction a sérialisé l'objet imbriqué résultant avec la même représentation JSON.

 // JSON imbriqué arbitrairement dd: = '"valeur": 3, "gauche": "valeur": 1, "gauche": null, "droite": "valeur": 2, "gauche": null, "right": null, "right": "value": 4, "left": null, "right": null 'interface var obj  err = json.Unmarshal ([] octet (dd) , & obj) if err! = nil fmt.Println (err) else fmt.Println ("-------- \ n", obj) données, err = json.Marshal (obj) si err ! = nil fmt.Println (err) else fmt.Println ("-------- \ n", string (data)) Sortie: -------- map [right : map [valeur: 4 restants: droite:] valeur: 3 à gauche: carte [à gauche: à droite: map [valeur: 2 à gauche: droite:] valeur: 1]] -------- "left": "left": null, "right": "left": null, "right": null, "value": 2, "valeur": 1, "droite": "gauche": null, "droite": null, "valeur": 4, "valeur": 3 

Pour parcourir les mappages génériques d'interfaces, vous devez utiliser des assertions de type. Par exemple:

func dump (obj interface ) erreur si obj == nil fmt.Println ("nil") renvoie nil commutateur obj. (type) case bool: fmt.Println (obj. (bool)) case int: fmt.Println (obj. (int)) case float64: fmt.Println (obj. (float64)) case string: fmt.Println (obj. (chaîne)) case map [chaîne] interface : pour k, v: = range (obj. (map [chaîne] interface )) fmt.Printf ("% s:", k) err: = dump (v) si err! = nil return err défaut: renvoie les erreurs. Nouveau (fmt.Sprintf ("Type non pris en charge:% v", obj)) return nil

Sérialisation des données structurées

Travailler avec des données structurées est souvent le meilleur choix. Go fournit un excellent support pour la sérialisation JSON de / vers structures via ses struct Mots clés. Créons un struct qui correspond à notre arbre JSON et un plus intelligent Déverser() fonction qui l’imprime:

type Arbre struct valeur int gauche * Arbre droite * Arbre func (t * Arbre) Dump (chaîne d'indentation) fmt.Println (indent + "valeur:", t.valeur) fmt.Print (indent + "left:" ) si t.left == nil fmt.Println (nil) else fmt.Println () t.left.Dump (indent + "") fmt.Print (indent + "right:") si t.right == nil fmt.Println (nil) else fmt.Println () t.right.Dump (indent + "") 

C’est génial et bien plus propre que l’approche JSON arbitraire. Mais ça marche? Pas vraiment. Il n'y a pas d'erreur, mais notre objet arbre n'est pas rempli par le JSON..

 jsonTree: = '"valeur": 3, "gauche": "valeur": 1, "gauche": null, "droite": "valeur": 2, "gauche": null, "droite": null , "right": "value": 4, "left": null, "right": null 'arbre d'arbre err = json.Unmarshal ([] octet (dd), & arbre) si err! = nil fmt.Printf ("- Vous ne pouvez pas déserislize, erreur:% v \ n", err) else tree.Dump ("") Sortie: valeur: 0 à gauche:  droite:  

Le problème est que les champs de l'arbre sont privés. La sérialisation JSON fonctionne uniquement sur les champs publics. Pour que nous puissions faire le struct champs publics. Le paquet json est suffisamment intelligent pour convertir de manière transparente les clés minuscules "value", "left" et "right" en leurs noms de champs majuscules correspondants..

type Tree struct Valeur int 'json: "valeur"' Gauche * Arbre 'json: "gauche"' Droite * Arbre 'json: "droite"' Sortie: valeur: 3 gauche: valeur: 1 gauche:  à droite: valeur: 2 à gauche:  droite:  à droite: valeur: 4 à gauche:  droite:  

Le package json ignorera en silence les champs non mappés du fichier JSON ainsi que les champs privés de votre struct. Mais parfois, vous souhaiterez peut-être mapper des clés spécifiques du code JSON avec un champ portant un nom différent dans votre nom. struct. Vous pouvez utiliser struct balises pour cela. Par exemple, supposons que nous ajoutions un autre champ appelé "label" au JSON, mais nous devons le mapper sur un champ appelé "Tag" dans notre structure.. 

type Tree struct Valeur int Tag chaîne 'json: "label"' Gauche * Arbre Droite * Arbre func (t * Arbre) Dump (chaîne d'indentation) fmt.Println (indent + "valeur:", t.Valeur) if t.Tag! = "" fmt.Println (indent + "tag:", t.Tag) fmt.Print (indent + "left:") si t.Left == nil fmt.Println (nil) else fmt.Println () t.Left.Dump (indent + "") fmt.Print (indent + "right:") si t.Right == nil fmt.Println (nil) else fmt.Println () t.Right.Dump (indent + "") 

Voici le nouveau JSON avec le nœud racine de l’arborescence étiqueté «racine», sérialisé correctement dans le champ Balise et imprimé dans le résultat:

 dd: = '"label": "racine", "valeur": 3, "gauche": "valeur": 1, "gauche": null, "droite": "valeur": 2, "gauche" : null, "right": null, "right": "value": 4, "left": null, "right": null 'arbre var Arbre err = json.Unmarshal ([] octet (dd ), & tree) if err! = nil fmt.Printf ("- Vous ne pouvez pas déserislize, erreur:% v \ n", err) else tree.Dump ("") Sortie: valeur: 3 balise: racine gauche: valeur: 1 gauche:  à droite: valeur: 2 à gauche:  droite:  à droite: valeur: 4 à gauche:  droite: 

Ecrire un Marshaller personnalisé

Vous voudrez souvent sérialiser des objets qui ne sont pas conformes aux exigences strictes de la fonction Marshal (). Par exemple, vous pouvez vouloir sérialiser une carte avec des clés int. Dans ces cas, vous pouvez écrire un marshaller / unmarshaller personnalisé en implémentant le Marshaler et Unmarshaler interfaces.

Remarque à propos de l'orthographe: Dans Go, la convention est de nommer une interface avec une seule méthode en ajoutant le suffixe "er" au nom de la méthode. Ainsi, même si l'orthographe la plus courante est "Marshaller" (avec un double L), le nom de l'interface est simplement "Marshaler" (un seul L)..

Voici les interfaces Marshaler et Unmarshaler:

type interface Marshaler MarshalJSON () ([] octet, erreur) type interface Unmarshaler erreur UnmarshalJSON ([] octet) 

Vous devez créer un type lorsque vous effectuez une sérialisation personnalisée, même si vous souhaitez sérialiser un type intégré ou une composition de types intégrés tels que map [int] chaîne. Ici, je définis un type appelé Carte IntString et mettre en œuvre le Marshaler et Unmarshaler interfaces pour ce type.

le MarshalJSON () méthode crée un map [chaîne] chaîne, convertit chacune de ses propres clés int en une chaîne et sérialise la carte avec des clés de chaîne en utilisant le standard json.Marshal () une fonction.

type IntStringMap map [int] chaîne func (m * IntStringMap) MarshalJSON () ([] octet, erreur) ss: = map [chaîne] chaîne  pour k, v: = plage * m i: = strconv.Itoa (k) ss [i] = v retourne json.Marshal (ss) 

La méthode UnmarshalJSON () fait exactement le contraire. Il désérialise le tableau d'octets de données en un map [chaîne] chaîne puis convertit chaque clé de chaîne en un int et se remplit.

func (m * IntStringMap) UnmarshalJSON (data [] byte) erreur ss: = map [chaîne] chaîne  err: = json.Unmarshal (données, & ss) si err! = nil return err pour k, v: = plage ss i, err: = strconv.Atoi (k) si err! = nil return err (* m) [i] = v return nil 

Voici comment l'utiliser dans un programme:

 m: = IntStringMap 4: "quatre", 5: "cinq" données, err: = m.MarshalJSON () si err! = nil fmt.Println (err) fmt.Println ("IntStringMap à JSON:" , chaîne (données)) m = IntStringMap  jsonString: = [] octet ("\" 1 \ ": \" un \ ", \" 2 \ ": \" deux \ "") m.UnmarshalJSON ( jsonString) fmt.Printf ("IntStringMap à partir de JSON:% v \ n", m) fmt.Println ("m [1]:", m [1], "m [2]:", m [2]) en sortie : IntStringMap à JSON: "4": "quatre", "5": "cinq" IntStringMap à partir de JSON: map [2: deux 1: un] m [1]: un m [2]: deux

Sérialisation des Enums

Aller enums peut être assez vexant de sérialiser. L'idée d'écrire un article sur la sérialisation Go json est née d'une question qu'un collègue m'avait posée sur la façon de sérialiser des enums. Voici un aller enum. Les constantes Zero et One sont égales aux ints 0 et 1.

tapez EnumType int const (Zero EnumType = iota One) 

Vous pensez peut-être que c'est un int, et à bien des égards, vous ne pouvez pas le sérialiser directement. Vous devez écrire un marshaler / unmarshaler personnalisé. Ce n'est pas un problème après la dernière section. Le suivant MarshalJSON () et UnmarshalJSON () sérialisera / désérialisera les constantes ZERO et ONE vers / à partir des chaînes correspondantes "Zero" et "One".

func (e * EnumType) UnmarshalJSON (data [] byte) erreur var s chaîne err: = json.Unmarshal (données, & s) si err! = nil retour err valeur, ok: = map [chaîne] EnumType " Zero ": Zero," One ": One [s] if! Ok return errors.New (" Valeur EnumType non valide ") * e = value return nil func (e * EnumType) MarshalJSON () ([] octet , erreur) valeur, ok: = carte [EnumType] chaîne Zero: "Zero", One: "Un" [* e] if! ok return nil, errors.New ("Valeur EnumType non valide") return json.Marshal (valeur) 

Essayons d'intégrer ceci EnumType dans un struct et le sérialiser. La fonction principale crée un EnumContainer et l'initialise avec un nom "Uno" et une valeur de notre enum constant UN, qui est égal à l'int 1.

type EnumContainer struct Nom chaîne Valeur EnumType func main () x: = Un ec: = EnumContainer "Uno", x, s, err: = json.Marshal (ec) si err! = nil fmt.Printf ("fail!") var ec2 EnumContainer err = json.Unmarshal (s, & ec2) fmt.Println (ec2.Name, ":", ec2.Value) Sortie: Uno: 0 

La sortie attendue est "Uno: 1", mais à la place c'est "Uno: 0". Qu'est-il arrivé? Il n'y a pas de bogue dans le code marshal / unmarshal. Il s'avère que vous ne pouvez pas incorporer d'énums par valeur si vous souhaitez les sérialiser. Vous devez intégrer un pointeur à l'énum. Voici une version modifiée où cela fonctionne comme prévu:

type EnumContainer struct Nom chaîne Valeur * EnumType func main () x: = Un ec: = EnumContainer "Uno", & x, s, err: = json.Marshal (ec) si err! = nil fmt. Printf ("fail!") Var ec2 EnumContainer err = json.Unmarshal (s, & ec2) fmt.Println (ec2.Name, ":", * ec2.Value) Sortie: Uno: 1

Conclusion

Go propose de nombreuses options pour la sérialisation et la désérialisation de JSON. Il est important de comprendre les tenants et les aboutissants du paquet encoding / json pour profiter de la puissance.

Ce tutoriel met tout le pouvoir entre vos mains, y compris comment sérialiser les enum insensibles.

Allez sérialiser des objets!