Codable + Extensions

SHARE

In this post, I'll try to add some useful extensions to make working with the Codable protocol easier and more practical. Let's start!

1. DecodeBool from Int or String

Because, you know, sometimes the backend guys send us a 1 or TRUE and expect us to map it to native Bool object, well, they are kinda right, it shouldn't matter that much:

public extension KeyedDecodingContainer where Key: CodingKey {

    /// Try to decode a Bool as Int then String before decoding as Bool.
    ///
    /// - Parameter key: Key.
    /// - Returns: Decoded Bool value.
    /// - Throws: Decoding error.
    public func decodeBoolAsIntOrString(forKey key: K) throws -> Bool {
        if let bool = try? decode(Bool.self, forKey: key) {
            return bool
        }
        if let bool = try? decode(String.self, forKey: key) {
            return bool == "1"
        }
        let int = try decode(Int.self, forKey: key)
        return int == 1
    }

    /// Try to decode a Bool as Int then String before decoding as Bool if present.
    ///
    /// - Parameter key: Key.
    /// - Returns: Decoded Bool value.
    /// - Throws: Decoding error.
    public func decodeBoolAsIntOrStringIfPresent(forKey key: K) throws -> Bool? {
        if let bool = try? decodeIfPresent(Bool.self, forKey: key) {
            return bool
        }
        if let bool = try? decodeIfPresent(String.self, forKey: key) {
            return bool == "1"
        }
        if let int = try? decodeIfPresent(Int.self, forKey: key) {
            return int == 1
        }
        return nil
    }

}

2. Decode URL by adding a path to a base URL

Useful when the URL is just a path that is required to be added to a base URL to form the full URL

public extension KeyedDecodingContainer where Key: CodingKey {

    /// Decode a URL path with appending an optional base URL.
    ///
    /// - Parameters:
    ///   - baseUrl: Base URL
    ///   - key: Key
    /// - Returns: Decoded URL.
    /// - Throws: Decoding error.
    public func decodeURL(baseUrl: URL?, forKey key: K) throws -> URL? {
        let path = try decode(String.self, forKey: key)
        return baseUrl?.appendingPathComponent(path)
    }

    /// Decode a URL path with appending an optional base URL (if URL present).
    ///
    /// - Parameters:
    ///   - baseUrl: Base URL
    ///   - key: Key
    /// - Returns: Decoded URL.
    /// - Throws: Decoding error.
    public func decodeURLIfPresent(baseUrl: URL?, forKey key: K) throws -> URL? {
        guard let path = try decodeIfPresent(String.self, forKey: key) else { return nil }
        return baseUrl?.appendingPathComponent(path)
    }

}

public extension KeyedEncodingContainer where Key: CodingKey {

    /// Encode URL if present and move the Base URL from it.
    ///
    /// - Parameters:
    ///   - url: Optional URL.
    ///   - baseUrl: Base URL.
    ///   - key: Key.
    /// - Throws: Encoding error.
    public mutating func encodeURLIfPresent(_ url: URL?, baseUrl: URL?, forKey key: K) throws {
        guard var string = url?.absoluteString else { return }
        guard let baseString = baseUrl?.absoluteString else { return }
        string.removeFirst(baseString.count)
        try encodeIfPresent(string, forKey: key)
    }

}

3. Save and retrieve Codable objects to UserDefaults

Because why not 😂

public extension UserDefaults {

    /// Retrieves a Codable object from UserDefaults.
    ///
    /// - Parameters:
    ///   - type: Class that conforms to the Codable protocol.
    ///   - key: Identifier of the object.
    ///   - decoder: Custom JSONDecoder instance. Defaults to `JSONDecoder()`.
    /// - Returns: Codable object for key (if exists).
    public func object<T: Codable>(_ type: T.Type, with key: String, usingDecoder decoder: JSONDecoder = JSONDecoder()) -> T? {
        guard let data = value(forKey: key) as? Data else { return nil }
        return try? decoder.decode(type.self, from: data)
    }

    /// Allows storing of Codable objects to UserDefaults.
    ///
    /// - Parameters:
    ///   - object: Codable object to store.
    ///   - key: Identifier of the object.
    ///   - encoder: Custom JSONEncoder instance. Defaults to `JSONEncoder()`.
    public func set<T: Codable>(object: T, forKey key: String, usingEncoder encoder: JSONEncoder = JSONEncoder()) {
        let data = try? encoder.encode(object)
        set(data, forKey: key)
    }

}

Example

Let's say we have an object called Image that has an id, url, and a boolean property indicating whether is it a gif or not, with the following JSON:

{
    "id": "test",
    "url": "test.png",
    "is_gif": 0
}

Here is how we can use the above extensions to map the object, even more, save, and retrieve it from UserDefaults:

struct Image: Codable {

    let id: String
    let url: URL?
    let isGif: Bool

    enum CodingKeys: String, CodingKey {
        case id
        case url
        case is_gif
    }

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

        id = try container.decode(String.self, forKey: .id)

        let baseUrl = URL(string: "your base url")!
        url = try container.decodeURL(baseUrl: baseUrl, forKey: .url)
        isGif = try container.decodeBoolAsIntOrString(forKey: .is_gif)
    }

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

        try container.encode(id, forKey: .id)

        let baseUrl = URL(string: "your base url")!
        try container.encodeURLIfPresent(url, baseUrl: baseUrl, forKey: .url)
        try container.encode(isGif, forKey: .is_gif)
    }

    static func save(_ image: Image) {
        UserDefaults.standard.set(object: self, forKey: image.id)
    }

    static func image(for id: String) -> Image? {
        return UserDefaults.standard.object(Image.self, with: id)
    }

}

That's it for now. I'll be adding more extensions here, feel free to save this page and come back from time to time if you like the above extensions!

If you know other useful extensions or tips for using the Codable protocol, please let me know via Twitter!

SoftwareSwiftUIKitiOS

You made it to the end. You're Awesome!

Here is something more to read

Protocol Oriented Extensions

Use the power of protocols and generic types to avoid extension conflicts

Making MVC Great Again!

Use generics, protocols, and extensions to get rid of massive view controllers

This is a fully integrated open-source project that uses NextJS, Redux, and Django to build. Grab your copy from Github

Copyright © 2019 Omar Albeik. All rights reserved.