backend / go
JSON encoding et décodage
Explication
Ce que vous allez apprendre
- Contrôler la sérialisation JSON d'une struct avec des tags (
json:"nom") - Exclure un champ sensible de la sérialisation avec
json:"-" - Décoder un JSON de forme inconnue avec
map[string]any - Reporter le décodage d'une sous-partie avec
json.RawMessage - Personnaliser le format de sortie avec une méthode
MarshalJSON
Dans quel contexte ?
Une API renvoie accidentellement le champ mot_de_passe (même haché) dans sa réponse JSON à un client, simplement parce que le développeur a oublié d'exclure ce champ de la struct Utilisateur. Ce type d'incident de sécurité, fréquent et évitable, est exactement ce que le tag json:"-" empêche dès la définition de la struct — une ligne de code qui vaut la peine d'être vérifiée systématiquement en revue de code.
D'abord, les tags de struct pilotent tout le mapping JSON
Sans tag, encoding/json utilise le nom exact du champ Go (avec sa majuscule) comme clé JSON. Le tag json:"nom_json" permet de choisir une clé différente, généralement en snake_case pour respecter les conventions REST habituelles, différentes du PascalCase idiomatique de Go.
Une fois ce mapping de base posé, plusieurs options de tag affinent le comportement
omitempty retire le champ du JSON produit si sa valeur est la zero value de son type (chaîne vide, zéro, slice vide...). json:"-" exclut totalement le champ, dans les deux sens (sérialisation et désérialisation) — la protection la plus stricte pour un champ sensible.
| Tag | Effet | Cas d'usage |
|---|---|---|
json:"nom" | Renomme la clé JSON | Respecter une convention snake_case côté API |
json:"email,omitempty" | Omet le champ si vide | Champs optionnels dans la réponse |
json:"-" | Exclut totalement le champ | Mots de passe, secrets internes |
| (aucun tag) | Utilise le nom Go tel quel | Rarement souhaitable pour une vraie API publique |
Prérequis
Il faut être à l'aise avec les structs (leçon dédiée) et avoir une première expérience de net/http (leçon précédente) pour situer où ce décodage JSON intervient concrètement.
Il reste une situation fréquente à savoir gérer : le schéma inconnu
Quand la forme exacte d'un JSON n'est pas connue à l'avance (un webhook externe, une configuration dynamique), décoder directement dans map[string]any permet d'explorer les clés une par une sans définir de struct à l'avance. C'est flexible, mais on perd toute vérification de type à la compilation : chaque valeur doit être "type assertée" manuellement à l'usage.
Piège fréquent
Décoder un nombre JSON dans map[string]any produit systématiquement un float64, jamais un int, même si la valeur JSON ressemble à un entier. Oublier cette conversion cause des bugs de type assertion (v.(int) qui échoue silencieusement) très fréquents chez les débutants qui manipulent du JSON dynamique.
Enfin, deux outils pour des cas plus avancés
json.RawMessage reporte le décodage d'une sous-partie d'un JSON, utile pour des payloads polymorphes où le champ type détermine comment interpréter le reste (donnees). Une méthode MarshalJSON personnalisée, elle, donne un contrôle total sur le format de sortie d'un type, par exemple pour sérialiser une valeur métier sous une forme différente de sa représentation interne.
Bonne pratique
Utilise MarshalIndent pendant le développement et le débogage pour obtenir un JSON lisible avec indentation, mais garde Marshal (compact) pour les réponses HTTP réelles en production, où chaque octet transféré compte.
Maintenant que tu maîtrises la sérialisation des données, la prochaine leçon aborde un mécanisme de contrôle de flux radicalement différent : defer, panic et recover, l'équivalent Go du try/catch.
Commandes & code
JSON encoding et décodage
Le package encoding/json gère la sérialisation via des tags de struct.
package main
import (
"encoding/json"
"fmt"
"time"
)
type Adresse struct {
Ville string `json:"ville"`
CP string `json:"code_postal"`
}
type Utilisateur struct {
ID int `json:"id"`
Nom string `json:"nom"`
Email string `json:"email,omitempty"` // omis si vide
MotDePasse string `json:"-"` // JAMAIS serialise
Adresse Adresse `json:"adresse"`
CreeLe time.Time `json:"cree_le"`
Tags []string `json:"tags,omitempty"`
}
func main() {
u := Utilisateur{
ID: 1,
Nom: "Alice",
MotDePasse: "secret123",
Adresse: Adresse{Ville: "Paris", CP: "75001"},
CreeLe: time.Now(),
Tags: []string{"admin", "beta"},
}
// Marshal : struct -> JSON
data, err := json.Marshal(u)
if err != nil {
panic(err)
}
fmt.Println(string(data)) // "mot_de_passe" absent grace a json:"-"
// MarshalIndent : JSON lisible pour debug/logs
pretty, _ := json.MarshalIndent(u, "", " ")
fmt.Println(string(pretty))
// Unmarshal : JSON -> struct
entree := `{"id":2,"nom":"Bob","adresse":{"ville":"Lyon","code_postal":"69000"}}`
var u2 Utilisateur
if err := json.Unmarshal([]byte(entree), &u2); err != nil {
panic(err)
}
fmt.Println(u2.Nom, u2.Adresse.Ville)
// Decoder JSON dynamique avec map[string]any quand le schema est inconnu
var donnees map[string]any
json.Unmarshal([]byte(`{"x":1,"y":"texte","z":[1,2,3]}`), &donnees)
for k, v := range donnees {
fmt.Printf("%s: %v (%T)\n", k, v, v)
}
// json.RawMessage : reporter le decodage d'une partie du JSON
type Enveloppe struct {
Type string `json:"type"`
Donnees json.RawMessage `json:"donnees"`
}
brut := `{"type":"utilisateur","donnees":{"id":3,"nom":"Carla"}}`
var env Enveloppe
json.Unmarshal([]byte(brut), &env)
if env.Type == "utilisateur" {
var u3 Utilisateur
json.Unmarshal(env.Donnees, &u3)
fmt.Println(u3.Nom)
}
// Interface personnalisee de (de)serialisation
fmt.Println(NouveauStatut("actif").Valide())
}
type Statut string
const (
StatutActif Statut = "actif"
StatutInactif Statut = "inactif"
)
func NouveauStatut(s string) Statut { return Statut(s) }
func (s Statut) Valide() bool { return s == StatutActif || s == StatutInactif }
// MarshalJSON personnalise : controle total du format de sortie
func (s Statut) MarshalJSON() ([]byte, error) {
return json.Marshal(string(s))
}Résumé
- Les tags
json:"nom"contrôlent le mapping des champs ;omitemptyet-gèrent les cas particuliers. Marshal/Unmarshalopèrent sur des[]byte;NewEncoder/NewDecodersur des flux (io.Writer/Reader).map[string]anydécode un JSON de forme inconnue à l'avance.json.RawMessagereporte le décodage d'une sous-partie, utile pour des payloads polymorphes.
Exercices pratiques
Mission : un mot de passe haché renvoyé par erreur à un client mobile
Objectif : Corriger une fuite de champ sensible dans une réponse JSON, puis fiabiliser le décodage d'un webhook au schéma partiellement inconnu.
Contexte
Un audit de sécurité découvre que l'endpoint GET /profil de l'application mobile renvoie le champ mot_de_passe (la version hachée, mais quand même) dans sa réponse JSON. La struct Utilisateur déclare le champ MotDePasse avec le tag json:"mot_de_passe", sans aucune protection particulière.