CodeGym /Courses /Swift SELF /JSONEncoder — encode, prettyPrinted, sortedKeys

JSONEncoder — encode, prettyPrinted, sortedKeys

Swift SELF
Level 58, Lesson 2
Available

1. Why do we need JSONEncoder if JSON is just text

Honestly, the first temptation for a beginner is: “I’ll just assemble JSON by hand with print and interpolation, how hard can it be?” And this is the moment when an experienced developer stands up, silently walks to the window, and stares into the distance, remembering all the bugs that started with the word “simple.”

In a real application (and we are continuing to develop our CLI project LibraryCLI), it is important to know how to turn typed models into a standardized data format. And this is where JSONEncoder is not a “convenient toy,” but a Foundation mechanism that encodes an Encodable value into JSON.

The key idea: JSONEncoder encodes a value into Data, that is, into bytes. This matters because in real applications JSON usually lives exactly in the form of bytes: it is sent over the network, written to a file, put into cache, and so on.

2. encode(_:): model → Data and why it is throws

The basic contract of JSONEncoder is the encode(_:) method -> Data. It returns Data, not String.

When you first see encode, your brain asks the classic question: “Why does it throw an error if I am encoding my own object?” And that is a good question: it seems that if the object is already in our hands, then “what could go wrong.”

In practice, encode(_:) is marked as throws because the encoding process can actually fail: for example, if the model contains values that cannot be represented in JSON correctly, or if you use encoding settings that require additional checks. The fact that the encode method returns Data and is throws is built into the design of JSONEncoder.

Let’s start with a minimal example (without complex domain types — today our goal is exactly JSONEncoder, not “the ideal model of the world”).

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) // the size will depend on the contents
} catch {
    print("Encoding failed:", error)
}

Pay attention to two things.

First, JSONEncoder is in Foundation, so import Foundation is required.

Second, the result is bytes (Data). Printing data.count honestly shows that we got “something binary,” not “a nice JSON string.”

3. How to view JSON: Data → String

Almost always, when you are learning serialization, you want to “just see what happened.” And that is completely normal: a person needs to see the result, otherwise the brain starts suspecting it is being fooled.

But there is a nuance: JSONEncoder returns Data, and Data is bytes. To see JSON as text, we need to convert Data → String through UTF-8. And here safety appears again: String(data:encoding:) returns String?, because data can in principle be invalid text.

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)
}

Yes, the JSON looks “compressed” and without spaces. That is normal: by default, JSONEncoder tries to produce compact output — machines do not need spaces, and files/network responses like to be smaller.

4. outputFormatting and .prettyPrinted

Now let’s take a step from “it works” to “you can live with this.” Because compact JSON is useful for transport and storage, but for human reading it looks like one long line.

For this, JSONEncoder has an output formatting setting — outputFormatting. It includes the .prettyPrinted option, which makes JSON “human-readable”: it adds line breaks and indentation.

The idea itself is simple: we are not changing the data, we are changing only the representation.

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"
}
*/

Notice the style: now JSON is printed with indentation and line breaks. Such output is convenient for logging, inserting into reports, comparing by eye, and simply “understanding what is happening.”

To reinforce the difference, it is useful to imagine this as a table:

Setting outputFormatting What JSON looks like What it is good for
(default) One line, minimum spaces compactness
.prettyPrinted
multi-line JSON with indentation human reading, debugging

And yes, in real life you may use one in logs and the other in “production” storage. Today we are stating this as an idea, but not building a large system around it so that we do not drift too far from the topic.

5. .sortedKeys and stable key order

The next setting looks strange until you step on the first rake. Imagine: you encode the same object twice and compare the results as strings (or as Data). But the result is somehow different. Panic? No. Most likely, the key order just changed.

To make the result stable, there is the .sortedKeys option: it forces the encoder to output dictionary keys in lexicographic order. In the implementation of JSONEncoder, this is described as “Produce JSON with dictionary keys sorted in lexicographic order”.

Let’s show it with an example. Add a few fields so that sorting is noticeable, and enable both “beauty” and “stability” at the same time:

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) ?? "")

What is important to understand conceptually: .sortedKeys is not an “essential part of JSON.” JSON as a format does not require key sorting. This is purely an engineering option that is useful when you need the same data to produce as identical a text as possible (for example, for comparisons, logs, and predictable output).

Another nuance that is useful to know in advance: .sortedKeys is marked as unavailable on some very old platforms. In the context of our educational CLI on current Swift this is usually not a problem, but the principle of “check API availability” is worth getting used to early.

6. Practice: diagram and debug utility

Data flow diagram

To avoid getting lost, it is useful to record what we actually did today: we learned how to get JSON in memory and, if needed, print it as text.

Diagram:

flowchart LR
    A["Book (Codable)"] --> B["JSONEncoder.encode(book)"]
    B --> C["Data (JSON bytes)"]
    C --> D["String(data:encoding:) for viewing"]

This diagram is simple, but it saves you from a common confusion: JSONEncoder does not print a string — it creates Data, and then you decide what to do next: display it, save it, or send it.

Mini utility for LibraryCLI: “show JSON nicely”

As the project grows, a moment comes when repeating the same do/catch and the Data → String conversion in every place becomes boring. And boredom is the main engine of refactoring.

Let’s make a small function that encodes a value into a JSON string for diagnostics. We intentionally return String, because this is a utility for “looking with your eyes.” And we will set formatting so that the output is both pretty and 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)>"
    }
}

Notice the style: we do not propagate the error outward, but turn it into text. This is not a “universal rule for all cases,” but a deliberate choice specifically for debug representation. If the task were “strictly encode and return outward,” we would design the interface differently.

Now let’s apply this to an array of books: an array [Book] is encoded perfectly well as a JSON array if 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))

When to enable prettyPrinted and sortedKeys

At this stage it is easy to start “optimizing beauty” and lose the point.

prettyPrinted exists so that humans do not suffer while looking at JSON. This is almost always about debugging, logs, and viewing the result by eye.

sortedKeys exists so that the result is more stable and predictable: keys are output in lexicographic order. This is convenient for comparing results, for diffs, and for cases where key order suddenly starts to matter (usually in tests or during manual diagnostics).

At the same time, you should not turn formatting into a religion: JSON does not become “smarter” because of it — it simply becomes more convenient to read or compare.

7. Common errors when working with JSONEncoder

Error #1: forgetting import Foundation and wondering why JSONEncoder “cannot be found.”
This is a classic: you write perfect code, the compiler complains, you start doubting reality... and then you notice that you forgot the import. JSONEncoder belongs to Foundation, and without import Foundation it is unavailable.

Error #2: expecting encode(_:) to return a string and trying to “print JSON right away.”
The encode(_:) method returns Data, not String. The encoder produces bytes, and you decide how to interpret them. If you do print(data), you will see the description of Data, but not guaranteed beautiful JSON text. The correct way to view it is String(data:encoding:).

Error #3: using try! for encoding “because my data is normal.”
Even if the data “seems normal,” encode is throws, and that means an error is possible and must be handled. In educational code, you want to shorten things, but in a real application try! turns a potentially manageable problem into a program crash.

Error #4: confusing “key order matters” with “key order does not matter.”
According to the JSON specification, the order of keys in an object should not be significant, but people and tools (diff, logs, string comparison) sometimes behave as if it were. If you need a stable representation, use .sortedKeys.

Error #5: enabling .prettyPrinted “just in case” and then being surprised by the size of the result.
.prettyPrinted adds spaces and line breaks, which means it makes Data larger. That is normal, but it is important to remember: this option is useful for humans, not for machines. If your goal is compactness, then pretty output may become unnecessary ballast.

1
Task
Swift SELF, level 58, lesson 2
Locked
Export profile
Export profile
1
Task
Swift SELF, level 58, lesson 2
Locked
Pretty JSON
Pretty JSON
1
Task
Swift SELF, level 58, lesson 2
Locked
Movie dump
Movie dump
1
Task
Swift SELF, level 58, lesson 2
Locked
Stable warehouse
Stable warehouse
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION