[SC]()

iOS. Apple. Indies. Plus Things.

@DefaultIfMissing - My Codable Failsafe

// Written by Jordan Morgan // Jun 20th, 2024 // Read it in about 3 minutes // RE: Swift

This post is brought to you by Emerge Tools, the best way to build on mobile.

Codable is worth its weight in proverbial gold. Saving developers across the globe the boilerplate code of manual .json serialization and deserialization code — it’s the first choice most of us turn to when decoding models over the wire.

But as mama told me growing up, there’s no such thing as a free lunch.

let concatenatedThoughts = """

If you aren't well-versed in the rigid ways of `Codable`, I'd invite you to read my extensive post on the matter here before continuing on.

"""

If you haven’t read the post linked above, let me sum up the fun part about being an iOS developer — the server will troll you. Maybe not today. Maybe not tomorrow. But probably when it’s a Friday night and you’re on call.

Alas:

struct Person: Codable, CustomStringConvertible {
    let name: String
    let age: Int
    
    var description: String { "\(name): \(age) years old" }
}

let mockResponse: Data = """
[
    {
        "name": "John Doe",
        "age": 30
    },
    {
        "name": "Jane Smith",
        "age": 25
    },
    {
        "name": "Alice Johnson",
        "age": 28
    }
]
""".data(using: .utf8)!

let decoder = JSONDecoder()

do {
    let people = try decoder.decode([Person].self, from: mockResponse)
    print(people)
} catch {
    print(error)
}

…decodes and works great.

But that Int property for age? It shows up in 3 out of 3 entries now. But what about when this happens tomorrow due to a bad deploy:

[
    {
        "name": "John Doe"
    },
    {
        "name": "Jane Smith",
        "age": 25
    },
    {
        "name": "Alice Johnson",
        "age": 28
    }
]

Wupps. Missing key! Now, none of the models would decode. Queue error alerts, or an empty data view, or the classic “Please reach out if this keeps happening!” text labels.

Further, what about when this happens:

[
    {
        "name": "John Doe",
        "age": 30
    },
    {
        "name": "Jane Smith",
        "age": 25
    },
    {
        "name": true,
        "age": 28
    }
]

Bummer, typeMismatch for the last name. Same thing, we get nothin’.

Look, sometimes (though not everytime), all a Swift developer wants is for primitives to freakin’ decode no matter what. And while you could write the encode and decode yourself, I don’t want to.

Enter @DefaultIfMissing, my little concoction to give primitives a default value if they are either missing in the response, or you get a whack value for them:

@propertyWrapper
struct DefaultIfMissing<T: Codable & DefaultValueProvider>: Codable {
    public let wrappedValue: T
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let value = try? container.decode(T.self) {
            wrappedValue = value
        } else {
            wrappedValue = T.defaultValue
        }
    }
    
    public init(_ wrappedValue: T?) {
        self.wrappedValue = wrappedValue ?? T.defaultValue
    }
    
    public init() {
        self.wrappedValue = T.defaultValue
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(wrappedValue)
    }
}

protocol DefaultValueProvider {
    static var defaultValue: Self { get }
}

extension Bool: DefaultValueProvider {
    static var defaultValue: Bool { false }
}

extension String: DefaultValueProvider {
    static var defaultValue: String { "" }
}

extension Int: DefaultValueProvider {
    static var defaultValue: Int { 0 }
}

extension Double: DefaultValueProvider {
    static var defaultValue: Double { 0.0 }
}

extension KeyedDecodingContainer {
    func decode<T: Codable & DefaultValueProvider>(_ type: DefaultIfMissing<T>.Type, forKey key: Key) throws -> DefaultIfMissing<T> {
        return try decodeIfPresent(type, forKey: key) ?? DefaultIfMissing(nil)
    }
}

Now, if I mark up Person like this, it’ll decode successfully with a default value for any responses that hand me back pancakes when I asked for biscuits:

struct Person: Codable, CustomStringConvertible {
    @DefaultIfMissing var name: String
    @DefaultIfMissing var age: Int
    
    var description: String { "\(name): \(age) years old" }
}

Now, if the response changed the type out from under me…

{
    "name": true,
    "age": 28
}

…I’d still get a back Person with a name set to an empty string, but has the age. And so on. In essence, this protects you against missing values or odd types for those values — and there are many times where this is exactly what I want.

Until next time ✌️

···

Spot an issue, anything to add?

Reach Out.