Annotating properties with default decoding values
Discover page available: CodableThe 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! 🚀