Sign in with GitHub to watch this episode for free.

Code Smells in Swift

Meaningless Fallback Values

1 Meaningless Fallback Values 06:54
2 Stringly Typed Code 06:24

You're probably familiar with Swift's nil-coalescing operator. The nil-coalescing operator returns a fallback value if the statement on its left doesn't produce a value. I use it quite often and there is nothing inherently wrong with it. The code smell we discuss in this episode of Code Smells in Swift is the value the statement on the right of the nil-coalescing operator produces, the fallback value.

Meaningless Fallback Values

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

Meaningless Fallback Values

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

import Foundation

We define a struct with name Book that conforms to the Decodable protocol. It declares three properties, id of type Int, title of type String, and author of type String. Note that the properties of the Book struct have a non-optional type, which means a Book object is required to have an id, a title, and an author.

import Foundation

struct Book: Decodable {

    // MARK: - Properties

    let id: Int
    let title: String
    let author: String

}

Even though conformance to the Decodable protocol is generated for us, we implement the required initializer of the Decodable protocol. We use the initializer of the Decodable protocol to illustrate the code smell. We create an extension for Book and declare a private enum with name CodingKeys that conforms to the CodingKey protocol. It defines the coding keys for the Book struct. The raw values of CodingKeys are of type String. We define a case for each property.

extension Book {

    // MARK: - Types

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

}

Implementing the initializer is straightforward. We ask the decoder for a keyed container and use the container to decode the values for the id, title, and author properties.

extension Book {

    // MARK: - Types

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

    // MARK: - Initialization

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

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

}

Let's assume the books are fetched from a remote API. We define a string literal and convert it to a Data object to mock the response of the remote API.

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

To convert the Data object to a Book object, 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 data =
"""
{
    "id": 1,
    "title": "The Missing Manual for Swift Development",
    "author": "Bart Jacobs"
}
""".data(using: .utf8)!

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

print(book)

The decoding operation succeeds without issues and the Book object is printed to the console.

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

Let's modify the response of the remote API by omitting the id field.

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

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

print(book)

The result of this change isn't surprising. The decoding operation fails since the id property is required to create a Book object.

Playground execution terminated: An error was thrown and was not caught:
▿ DecodingError
  ▿ keyNotFound : 2 elements
    - .0 : CodingKeys(stringValue: "id", intValue: nil)
    ▿ .1 : Context
      - codingPath : 0 elements
      - debugDescription : "No value associated with key CodingKeys(stringValue: \"id\", intValue: nil) (\"id\")."
      - underlyingError : nil

If this happens in production, your application may display an error to the user or, even worse, nothing. This is something we need to avoid so we need to prepare for this scenario. The problem is that the precautions developers take tend to be inappropriate. Take a look at the following solution.

extension Book {

    // MARK: - Types

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

    // MARK: - Initialization

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

        id = (try? container.decode(Int.self, forKey: .id)) ?? 0
        title = (try? container.decode(String.self, forKey: .title)) ?? ""
        author = try container.decodeIfPresent(String.self, forKey: .author) ?? ""
    }

}

The updated initializer contains two problems. First, the initializer assumes the id, title, and author fields may be null or missing. For that reason, the initializer uses the try keyword with a question mark and, for the author property, the decodeIfPresent(_:forKey:) method. This is a code smell because the declaration of Book defines the three properties as non-optional, that is, required. Second, because the initializer uses the try keyword with a question mark and the decodeIfPresent(_:forKey:) method, it uses the nil-coalescing operator with a fallback value. The fallback values are the code smell I would like to discuss in more detail.

What does it mean for a book to have 0 for an id or an empty string for title or author? The fallback values that are used in the initializer have no meaning. They are necessary because the initializer uses the try keyword with a question mark and the decodeIfPresent(_:forKey:) method.

Falling back to values that have no meaning is a code smell and should be avoided if possible. Because the application doesn't crash and no error is thrown, issues caused by meaningless fallback values can go unnoticed for quite some time. They are difficult to debug and usually result in a poor user experience.

Fixing the Code Smell

The solution is plain and simple. We revert the initializer to its original implementation. If the Book struct reflects the API contract, that is, the id, title, and author fields are required for a book, then the decoding operation should fail if one or more required fields are missing from the response returned by the remote API.

extension Book {

    // MARK: - Types

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

    // MARK: - Initialization

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

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

}

Decoding a response that doesn't include the id field results in an error and that is an appropriate response from the client. The remote API violates the contract it has with the client and that needs to be addressed on the backend. It is important to log errors like this to a remote server to make sure issues like this can be detected and addressed as soon as possible.

Defensive Coding

It is true that the code you write needs to be flexible and forgiving, but there are limitations. We could change the properties of the Book struct to be of an optional type. That would also address the code smell. That solution isn't ideal, though. A responsibility of the backend, response validation, is pushed to the client. Every client would need to take on that responsibility and the implementations would need to be aligned. That is something you need to avoid.

A proper API contract is indispensable. The API contract defines the type and nullability for every field of the response. Both parties need to enforce the API contract and that means the backend needs to validate the response it returns to the client.

What's Next?

The nil-coalescing operator is convenient, but it is important that you provide a fallback value that makes sense. An empty string is very often a code smell. It needs to be replaced with nil or a value that makes sense. If you don't have access to a value that makes sense, then throw an error.