1. Pourquoi avons-nous besoin de JSONEncoder si JSON n’est que du texte
Soyons francs, la première tentation d’un débutant, c’est : « allez, je vais assembler le JSON à la main avec print et l’interpolation, qu’est-ce qui pourrait être compliqué ? ». Et c’est précisément le moment où le développeur expérimenté se lève, s’approche en silence de la fenêtre et regarde au loin, en se souvenant de tous les bugs qui ont commencé par le mot « simple ».
Dans une application réelle (et nous continuons à faire évoluer notre projet CLI LibraryCLI), il est important de savoir transformer des modèles typés en format de données standardisé. Et ici, JSONEncoder n’est pas un « jouet pratique », mais un mécanisme de Foundation qui encode une valeur Encodable en JSON.
Idée clé : JSONEncoder code une valeur en Data, c’est-à-dire en octets. C’est important, car dans les applications réelles, JSON vit le plus souvent sous forme d’octets : on l’envoie sur le réseau, on l’écrit dans un fichier, on le place dans un cache, etc.
2. encode(_:) : modèle → Data et pourquoi c’est throws
Le contrat de base de JSONEncoder est la méthode encode(_:) -> Data. Elle renvoie Data, et non String.
Quand vous voyez encode pour la première fois, votre cerveau pose la question classique : « pourquoi cela lance-t-il une erreur si j’encode mon propre objet ? ». Et c’est une bonne question : on a l’impression que si l’objet est déjà entre nos mains, alors « qu’est-ce qui pourrait mal se passer ».
En pratique, encode(_:) est marqué comme throws, car le processus de codage peut réellement échouer : par exemple, si la modèle contient des valeurs impossibles à représenter correctement en JSON, ou si vous utilisez des paramètres de codage qui exigent des vérifications supplémentaires. Le fait même que la méthode encode renvoie Data et est throws est fixé dans la conception de JSONEncoder.
Commençons par un exemple minimal (sans types de domaine complexes — aujourd’hui, notre objectif est précisément JSONEncoder, et non « le modèle idéal du monde »).
import Foundation
struct Book: Codable {
let id: Int
let title: String
let author: String
}
let book = Book(id: 1, title: "Clean Code", author: "Robert C. Martin")
do {
let data = try JSONEncoder().encode(book)
print("Bytes:", data.count) // la taille dépendra du contenu
} catch {
print("Encoding failed:", error)
}
Remarquez deux choses.
Premièrement, JSONEncoder fait partie de Foundation, donc import Foundation est obligatoire.
Deuxièmement, le résultat est en octets (Data). L’affichage de data.count montre honnêtement que nous avons obtenu « quelque chose de binaire », et non une « jolie chaîne JSON ».
3. Comment voir le JSON : Data → String
Presque toujours, lorsqu’on apprend la sérialisation, on veut « simplement voir ce qui a été produit ». Et c’est tout à fait normal : il faut voir le résultat, sinon le cerveau commence à soupçonner qu’on le trompe.
Mais il y a un détail : JSONEncoder renvoie Data, et Data, ce sont des octets. Pour voir le JSON sous forme de texte, nous devons convertir Data → String via UTF-8. Et ici, la sécurité réapparaît : String(data:encoding:) renvoie String?, car les données peuvent en principe ne pas être un texte valide.
import Foundation
struct Book: Codable {
let id: Int
let title: String
let author: String
}
let book = Book(id: 1, title: "Clean Code", author: "Robert C. Martin")
do {
let data = try JSONEncoder().encode(book)
let jsonText = String(data: data, encoding: .utf8) ?? "<not UTF-8>"
print(jsonText) // {"id":1,"title":"Clean Code","author":"Robert C. Martin"}
} catch {
print("Encoding failed:", error)
}
Oui, le JSON a l’air « compact » et sans espaces. C’est normal : par défaut, JSONEncoder essaie de produire une sortie compacte — la machine n’a pas besoin d’espaces, et les fichiers / réponses réseau aiment être plus petits.
4. outputFormatting et .prettyPrinted
Passons maintenant de « ça marche » à « on peut vivre avec ». Car un JSON compact est utile pour le transfert et le stockage, mais pour la lecture humaine, il ressemble à une longue ligne.
Pour cela, JSONEncoder dispose d’un paramètre de formatage du résultat — outputFormatting. Il contient l’option .prettyPrinted, qui rend le JSON « lisible par l’humain » : elle ajoute des retours à la ligne et des indentations.
L’idée est simple : nous ne changeons pas les données, nous changeons seulement la représentation.
import Foundation
struct Book: Codable {
let id: Int
let title: String
let author: String
}
let book = Book(id: 1, title: "Clean Code", author: "Robert C. Martin")
var encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try encoder.encode(book)
print(String(data: data, encoding: .utf8) ?? "")
/*
{
"id" : 1,
"title" : "Clean Code",
"author" : "Robert C. Martin"
}
*/
Notez le style : désormais, le JSON s’affiche avec des indentations et des retours à la ligne. Une telle sortie est pratique à journaliser, à insérer dans des rapports, à comparer visuellement et, tout simplement, à « comprendre ce qui se passe ».
Pour bien fixer la différence, il est utile de la présenter sous forme de tableau :
| Paramètre outputFormatting | À quoi ressemble le JSON | À quoi cela sert |
|---|---|---|
| (par défaut) | Une seule ligne, un minimum d’espaces | compacité |
|
JSON multilignes avec des indentations | lecture humaine, débogage |
Et oui, dans la vraie vie, vous pouvez utiliser l’un dans les logs, l’autre pour le stockage « en production ». Aujourd’hui, nous l’évoquons comme une idée, sans construire un grand système autour, afin de ne pas nous éparpiller.
5. .sortedKeys et l’ordre stable des clés
Le réglage suivant paraît étrange, jusqu’au moment où l’on tombe sur les premiers pièges. Imaginez : vous encodez deux fois le même objet et comparez les résultats sous forme de chaînes (ou de Data). Et pourtant, le résultat est différent. Panique ? Non. Le plus probable est simplement que l’ordre des clés a changé.
Pour rendre le résultat stable, il existe l’option .sortedKeys : elle oblige l’encodeur à afficher les clés des dictionnaires dans l’ordre lexicographique. Dans l’implémentation de JSONEncoder, cela est décrit comme “Produce JSON with dictionary keys sorted in lexicographic order”.
Montrons cela sur un exemple. Ajoutons quelques champs afin que le tri soit visible, et activons en même temps la « beauté » et la « stabilité » :
import Foundation
struct Book: Codable {
let id: Int
let title: String
let author: String
let tags: [String]
}
let book = Book(
id: 2,
title: "Swift in Depth",
author: "Tjeerd in 't Veen",
tags: ["swift", "language"]
)
var encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let data = try encoder.encode(book)
print(String(data: data, encoding: .utf8) ?? "")
Ce qu’il faut comprendre conceptuellement : .sortedKeys n’est pas une « partie obligatoire du JSON ». Le format JSON n’exige pas que les clés soient triées. C’est purement une option d’ingénierie, utile lorsque vous voulez que des données identiques produisent un texte aussi identique que possible (par exemple pour la comparaison, les logs, la prévisibilité de la sortie).
Un autre détail utile à connaître à l’avance : .sortedKeys est signalé comme n’étant pas disponible sur toutes les très anciennes plateformes. Dans le contexte de notre CLI d’apprentissage sur un Swift récent, ce n’est généralement pas un problème, mais il vaut mieux s’habituer dès maintenant au principe « vérifier la disponibilité de l’API ».
6. Pratique : schéma et utilitaire de debug
Schéma du flux de données
Pour ne pas se perdre, il est utile de rappeler ce que nous avons réellement fait aujourd’hui : nous avons appris à obtenir du JSON en mémoire et, si besoin, à l’afficher comme du texte.
Schéma :
flowchart LR
A["Book (Codable)"] --> B["JSONEncoder.encode(book)"]
B --> C["Data (JSON bytes)"]
C --> D["String(data:encoding:) for viewing"]
Ce schéma est simple, mais il évite une confusion fréquente : JSONEncoder n’affiche pas une chaîne — il crée Data, puis c’est à vous de décider de la suite : afficher, sauvegarder, envoyer.
Mini utilitaire pour LibraryCLI : « montre le JSON joliment »
Avec la croissance du projet, arrive le moment où répéter le même do/catch et la conversion Data → String à chaque endroit devient ennuyeux. Et l’ennui est le principal moteur du refactoring.
Créons une petite fonction qui encode une valeur en chaîne JSON à des fins de diagnostic. Nous renvoyons volontairement String, car il s’agit d’un utilitaire pour « voir à l’œil ». Et nous définirons le formatage de manière à ce que la sortie soit à la fois jolie et stable.
import Foundation
func encodeToDebugJSONString<T: Encodable>(_ value: T) -> String {
var encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
do {
let data = try encoder.encode(value)
return String(data: data, encoding: .utf8) ?? "<not UTF-8>"
} catch {
return "<encoding failed: \\(error)>"
}
}
Remarquez le style : nous ne propageons pas l’erreur à l’extérieur, nous la transformons en texte. Ce n’est pas une « règle universelle pour tous les cas », mais un choix conscient précisément pour une représentation de debug. Si la tâche avait été « encoder strictement et renvoyer vers l’extérieur », nous aurions conçu l’interface autrement.
Appliquons maintenant cela à un tableau de livres : un tableau [Book] se code parfaitement en tableau JSON si Book: Codable.
import Foundation
struct Book: Codable {
let id: Int
let title: String
let author: String
let tags: [String]
}
let books = [
Book(id: 1, title: "Clean Code", author: "Robert C. Martin", tags: ["practice"]),
Book(id: 2, title: "Swift in Depth", author: "Tjeerd in 't Veen", tags: ["swift"])
]
print(encodeToDebugJSONString(books))
Quand activer prettyPrinted et sortedKeys
À ce stade, il est facile de commencer à « optimiser la beauté » et à perdre la substance.
prettyPrinted existe pour que l’humain ne souffre pas en regardant le JSON. C’est presque toujours une affaire de débogage, de logs et de consultation visuelle du résultat.
sortedKeys existe pour que le résultat soit plus stable et plus prévisible : les clés sont affichées dans l’ordre lexicographique. C’est pratique pour comparer des résultats, pour les diff et pour les cas où l’ordre des clés commence soudainement à avoir de l’importance (généralement dans les tests ou lors d’un diagnostic manuel).
Cela dit, il ne faut pas transformer le formatage en religion : le JSON ne devient pas « plus intelligent » pour autant — il devient simplement plus confortable à lire ou à comparer.
7. Erreurs typiques lors de l’utilisation de JSONEncoder
Erreur n°1 : oublier import Foundation et se demander pourquoi JSONEncoder « n’est pas trouvé ».
C’est un grand classique : vous écrivez un code parfait, le compilateur se plaint, vous commencez à douter de la réalité… puis vous remarquez que vous avez oublié l’import. JSONEncoder appartient à Foundation, et sans import Foundation, il n’est pas disponible.
Erreur n°2 : s’attendre à ce que encode(_:) renvoie une chaîne, et essayer de « simplement afficher le JSON ».
La méthode encode(_:) renvoie Data, et non String. L’encodeur fournit des octets, et c’est ensuite à vous de décider comment les interpréter. Si vous faites print(data), vous verrez la description de Data, mais pas nécessairement un beau texte JSON. La bonne manière de l’afficher est String(data:encoding:).
Erreur n°3 : utiliser try! pour le codage « parce que mes données sont normales ».
Même si les données « semblent normales », encode est throws, et cela signifie : une erreur est possible et doit être gérée. Dans le code d’apprentissage, on veut parfois raccourcir, mais dans une application réelle, try! transforme un problème potentiellement contrôlable en plantage du programme.
Erreur n°4 : confondre « l’ordre des clés est important » et « l’ordre des clés n’est pas important ».
Selon la spécification JSON, l’ordre des clés dans un objet ne devrait pas être significatif, mais les humains et les outils (diff, logs, comparaison de chaînes) se comportent parfois comme s’il l’était. Si vous avez besoin de stabilité dans la représentation, utilisez .sortedKeys.
Erreur n°5 : activer .prettyPrinted « au cas où » et s’étonner ensuite de la taille du résultat.
.prettyPrinted ajoute des espaces et des retours à la ligne, ce qui augmente la taille de Data. C’est normal, mais il faut garder à l’esprit que cette option est utile pour l’humain, pas pour la machine. Si votre objectif est la compacité, une belle sortie peut devenir un ballast superflu.
GO TO FULL VERSION