Weekly Swift articles, podcasts and tips by John Sundell.

Annotating properties with default decoding values

Published on 10 Jun 2020
Basics article available: Codable

The introduction of Codable back in 2017 was, without a doubt, a big leap forward for Swift. Although multiple tools for encoding and decoding native Swift values to and from JSON had already been built by the community at that point, Codable offered an unprecedented level of convenience due to its integration with the Swift compiler itself — enabling us to define decodable types simply by making them adopt the Decodable protocol, like this:

struct Article: Decodable {
    var title: String
    var body: String
    var isFeatured: Bool
}

However, one feature that has been missing from Codable ever since its introduction is the option to add default values to certain properties (without having to make them optionals). So for example, let’s say that the above isFeatured property won’t always appear in the JSON data that we’ll decode Article instances from, and that we want it to default to false in that case.

Even if we add that default value to our property declaration itself, the default decoding process will still fail in case that value is missing from our underlying JSON data:

struct Article: Decodable {
    var title: String
    var body: String
    var isFeatured: Bool = false // This value isn't used when decoding
}

Now, we could always write our own decoding code (by overriding the default implementation of init(from: Decoder)), but that would require us to take over the entire decoding process — which kind of ruins the whole convenience aspect of Codable, and would require us to keep updating that code for any change to our model’s properties.

The good news is that there’s another path that we can take, and that’s to use Swift’s property wrappers feature, which enables us to attach custom logic to any stored property. For example, we could use that feature to implement a DecodableBool wrapper, with false as its default value:

@propertyWrapper
struct DecodableBool {
    var wrappedValue = false
}

We could then make our new property wrapper conform to Decodable to enable it “take over” the decoding process for any property that it’s attached to. In this case, we do want to use a manual decoding implementation, as that’d let us decode instances directly from Bool values — like this:

extension DecodableBool: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        wrappedValue = try container.decode(Bool.self)
    }
}

The reason we conform to Decodable through an extension is to not override our type’s memberwise initializer.

Finally, we’ll also need to make Codable treat instances of the above property wrapper as optional during the decoding process, which can be done by extending the KeyedDecodingContainer type with an overload for decoding DecodableBool specifically — in which we only continue decoding in case a value exists for the given key, otherwise we’ll fall back to an empty instance of our wrapper:

extension KeyedDecodingContainer {
    func decode(_ type: DecodableBool.Type,
                forKey key: Key) throws -> DecodableBool {
        try decodeIfPresent(type, forKey: key) ?? .init()
    }
}

With the above in place, we can now simply annotate any Bool property with our new DecodableBool attribute — and it’ll default to false when it’s being decoded:

struct Article: Decodable {
    var title: String
    var body: String
    @DecodableBool var isFeatured: Bool
}

Really nice. However, while we’ve now solved this particular problem, our solution isn’t very flexible. What if we’d like true to be the default value in some cases, and what if we have non-Bool properties that we’d also like to provide default decoding values for?

So let’s see if we can generalize our solution into something that can be applied within a much larger range of situations. To do that, let’s start by creating a generic protocol for default value sources — which will enable us to define all sorts of defaults, not just boolean ones:

protocol DecodableDefaultSource {
    associatedtype Value: Decodable
    static var defaultValue: Value { get }
}

Then, let’s use an enum to create a namespace for the decoding code that we’re about to write — which will both give us a really nice syntax, and also provide a neat degree of code encapsulation:

enum DecodableDefault {}

The advantage of using case-less enums to implement namespaces is that they can’t be initialized, which makes them act as pure wrappers, rather than stand-alone types that can be instantiated.

The first type that we’ll add to our new namespace is a generic variant of our DecodableBool property wrapper from before — which now uses a DecodableDefaultSource to retrieve its default wrappedValue, like this:

extension DecodableDefault {
    @propertyWrapper
    struct Wrapper<Source: DecodableDefaultSource> {
        typealias Value = Source.Value
        var wrappedValue = Source.defaultValue
    }
}

Next, let’s make the above property wrapper conform to Decodable, and we’ll also implement another KeyedDecodingContainer overload that’s specific to our new type:

extension DecodableDefault.Wrapper: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        wrappedValue = try container.decode(Value.self)
    }
}

extension KeyedDecodingContainer {
    func decode<T>(_ type: DecodableDefault.Wrapper<T>.Type,
                   forKey key: Key) throws -> DecodableDefault.Wrapper<T> {
        try decodeIfPresent(type, forKey: key) ?? .init()
    }
}

With the above base infrastructure in place, let’s now go ahead and implement a few default value sources. We’ll again use an enum to provide an additional level of namespacing for our sources (just like Combine does for its publishers), and we’ll also add a few type aliases to make our code slightly easier to read:

extension DecodableDefault {
    typealias Source = DecodableDefaultSource
    typealias List = Decodable & ExpressibleByArrayLiteral
    typealias Map = Decodable & ExpressibleByDictionaryLiteral

    enum Sources {
        enum True: Source {
            static var defaultValue: Bool { true }
        }

        enum False: Source {
            static var defaultValue: Bool { false }
        }

        enum EmptyString: Source {
            static var defaultValue: String { "" }
        }

        enum EmptyList<T: List>: Source {
            static var defaultValue: T { [] }
        }

        enum EmptyMap<T: Map>: Source {
            static var defaultValue: T { [:] }
        }
    }
}

By constraining our EmptyList and EmptyMap types to two of Swift’s literal protocols, rather than concrete types like Array and Dictionary, we can cover a lot more ground — since many different types adopt those protocols, including Set, IndexPath, and more.

To wrap things up, let’s also define a series of convenience type aliases that’ll let us reference the above sources as specialized versions of our property wrapper type — like this:

extension DecodableDefault {
    typealias True = Wrapper<Sources.True>
    typealias False = Wrapper<Sources.False>
    typealias EmptyString = Wrapper<Sources.EmptyString>
    typealias EmptyList<T: List> = Wrapper<Sources.EmptyList<T>>
    typealias EmptyMap<T: Map> = Wrapper<Sources.EmptyMap<T>>
}

That last piece gives us a really nice syntax for annotating properties with decodable defaults, which can now simply be done like this:

struct Article: Decodable {
    var title: String
    @DecodableDefault.EmptyString var body: String
    @DecodableDefault.False var isFeatured: Bool
    @DecodableDefault.True var isActive: Bool
    @DecodableDefault.EmptyList var comments: [Comment]
    @DecodableDefault.EmptyMap var flags: [String : Bool]
}

Really neat, and perhaps the best part is that our solution is now truly generic — we can easily add new sources whenever we need to, all while keeping our call sites as clean as possible.

As a series of finishing touches, let’s also use Swift’s conditional conformances feature to make our property wrapper conform to common protocols — such as Equatable, Hashable and also Encodable — whenever its wrapped Value type does:

extension DecodableDefault.Wrapper: Equatable where Value: Equatable {}
extension DecodableDefault.Wrapper: Hashable where Value: Hashable {}

extension DecodableDefault.Wrapper: Encodable where Value: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(wrappedValue)
    }
}

And with that in place, we now have a finished solution for annotating properties with default decoding values — all without requiring any changes to the property types that are being decoded, and with a neatly encapsulated implementation, thanks to our DecodableDefault enum.

Thanks for reading! 🚀

Support Swift by Sundell by checking out this sponsor:

Instabug
Instabug

Instabug: Investigate, diagnose and resolve issues up to four times faster. Whether it’s a crash, slow screen transitions, slow network calls or unresponsive UIs, Instabug lets you utilize powerful performance patterns to trace the cause of each issue. Detect if a specific app version, device or network connection is affecting the user experience and spot trends and spikes. Get started now and ship apps that your users will love.