Sign in with GitHub to watch this episode for free.

Tips and Tricks

How to Encode Null Using JSONEncoder?

The JSONDecoder and JSONEncoder classes make working with JSON a breeze. The true star is Codable, though. Codable, a type alias for Decodable & Encodable, is flexible and provides a lot of functionality for very little effort. From time to time, you run into a situation that makes you scratch your head. This episode covers one such situation. How do you encode null using JSONEncoder? The good news is that the solution is fairly straightforward.

Decoding Using JSONDecoder

Fire up Xcode and create a playground using the Blank template from the iOS section.

How to Encode Null Using JSONEncoder

Remove the contents of the playground and add an import statement for the Foundation framework.

import Foundation

We define a struct, Book, that conforms to the Codable protocol. As I wrote earlier, Codable is a type alias for Decodable & Encodable, meaning that a type that conforms to Codable conforms to Decodable and Encodable.

import Foundation

struct Book: Codable {
	
}

The Book struct defines two properties of type String, title and author. The conformance to Decodable and Encodable is generated for us.

import Foundation

struct Book: Codable {

    // MARK: - Properties

    let title: String
    let author: String

}

Let's define a string literal and convert it to a Data object.

let data =
"""
{
    "title": "The Missing Manual for Swift Development",
    "author": "Bart Jacobs"
}
""".data(using: .utf8)!

We create a JSONDecoder instance and pass the Data object to the decode(_:from:) method. We instruct the JSON decoder to decode a Book object from the Data object.

let book = try JSONDecoder().decode(Book.self, from: data)

print(book)

This is what the output looks like in Xcode's console.

Book(title: "The Missing Manual for Swift Development", author: "Bart Jacobs")

Encoding Using JSONEncoder

Let's reverse the operation. We create a Book object, create a JSONEncoder instance, and pass the Book object to its encode(_:) method. We set the JSON encoder's outputFormatting to prettyPrinted to make the output in the console easier to read. We print the Data object as a string.

let book = Book(
    title: "The Missing Manual for Swift Development",
    author: "Bart Jacobs"
)

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted

let data = try encoder.encode(book)

print(String(data: data, encoding: .utf8)!)

The output is exactly what we expect.

{
  "title" : "The Missing Manual for Swift Development",
  "author" : "Bart Jacobs"
}

Let's say that it is possible for a book to be part of a series. Even though this book isn't part of a series, the API we submit the book to expects the following JSON payload. This isn't supported out of the box so we need to make a few changes.

{
  "title" : "The Missing Manual for Swift Development",
  "author" : "Bart Jacobs",
  "series": null
}

Adding an Optional Property

We explore two solutions. The easiest solution is to declare a property series of type String?.

import Foundation

struct Book: Codable {

    // MARK: - Properties

    let title: String
    let author: String
    let series: String?

}

We also need to update the initializer of the Book struct, passing in nil for the series parameter.

let book = Book(
    title: "The Missing Manual for Swift Development",
    author: "Bart Jacobs",
    series: nil
)

You may thing this is sufficient, but it isn't. Take a look at the output. We need to make a few more changes.

{
  "title" : "The Missing Manual for Swift Development",
  "author" : "Bart Jacobs"
}

The series field is missing from the JSON payload. To remedy that, we need to implement the encode(to:) method of the Encodable protocol. This also means we need to define the coding keys for the Book struct.

struct Book: Codable {

    // MARK: - Types

    private enum CodingKeys: String, CodingKey {
        case title, author, series
    }

    // MARK: - Properties

    let title: String
    let author: String
    let series: String?

    // MARK: - Encoding

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(title, forKey: .title)
        try container.encode(author, forKey: .author)
        try container.encode(series, forKey: .series)
    }

}

Implementing the encode(to:) method does the trick as the output illustrates.

{
  "title" : "The Missing Manual for Swift Development",
  "author" : "Bart Jacobs",
  "series" : null
}

This introduces a subtle code smell, though. The issue with this solution is that we declare the series property for the sole purpose of resolving an encoding issue. This may be confusing since the series property isn't being used in the project. Let's clean up the API and get rid of the code smell. We first remove the series property.

struct Book: Codable {

    // MARK: - Types

    private enum CodingKeys: String, CodingKey {
        case title, author, series
    }

    // MARK: - Properties

    let title: String
    let author: String

    // MARK: - Encoding

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(title, forKey: .title)
        try container.encode(author, forKey: .author)
        try container.encode(series, forKey: .series)
    }

}

Don't forget to update the initializer.

let book = Book(
    title: "The Missing Manual for Swift Development",
    author: "Bart Jacobs"
)

The compiler doesn't like this and throws a few errors. Let's tackle the errors one by one.

You may think we need to pass nil to the encode(_:forKey:) method, but that results in another error.

The solution is to define a local constant, series, of type String?, set it to nil, and pass the constant to the encode(_:forKey:) method.

struct Book: Codable {

    // MARK: - Types

    private enum CodingKeys: String, CodingKey {
        case title, author, series
    }

    // MARK: - Properties

    let title: String
    let author: String

    // MARK: - Encoding

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(title, forKey: .title)
        try container.encode(author, forKey: .author)

        let series: String? = nil
        try container.encode(series, forKey: .series)
    }

}

That change resolves the first error. Let's take a look at the second error. The compiler complains the Book struct no longer conforms to the Decodable protocol. Why is that?

The CodingKeys enum defines three cases. If each case corresponds with a property, the conformance to the Decodable protocol can be generated for us. That condition is no longer true, though. Because we removed the series property, we need to implement the init(from:) method of the Decodable protocol.

We implement the initializer in an extension to ensure we don't lose the memberwise initializer that we get for free. You can read more about structs and memberwise initializers in What Is a Memberwise Initializer.

The implementation of the init(from:) initializer isn't complex as you can see. Notice that we simply ignore the series case in the initializer.

extension Book {

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        title = try container.decode(String.self, forKey: .title)
        author = try container.decode(String.self, forKey: .author)
    }

}

This change resolve the second error and the resulting JSON payload matches what we expected.

{
  "title" : "The Missing Manual for Swift Development",
  "author" : "Bart Jacobs",
  "series" : null
}

What's Next?

Which solution you prefer depends on your requirements. If the optional property isn't an issue, then the first solution is sufficient. If the optional property may cause confusion, then the second solution is a better fit. While it may be tempting to opt for the first solution, make sure the API you create is clean and clear. Don't opt for the first solution because it requires less code.