Retour au cours

backend / go

JSON encoding et décodage

Leçon 211 exercice

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.

TagEffetCas d'usage
json:"nom"Renomme la clé JSONRespecter une convention snake_case côté API
json:"email,omitempty"Omet le champ si videChamps optionnels dans la réponse
json:"-"Exclut totalement le champMots de passe, secrets internes
(aucun tag)Utilise le nom Go tel quelRarement 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.

go
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 ; omitempty et - gèrent les cas particuliers.
  • Marshal/Unmarshal opèrent sur des []byte ; NewEncoder/NewDecoder sur des flux (io.Writer/Reader).
  • map[string]any décode un JSON de forme inconnue à l'avance.
  • json.RawMessage reporte le décodage d'une sous-partie, utile pour des payloads polymorphes.

Exercices pratiques

1 disponible
1

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.

Résoudre l’exercice →