Codable + Extensions

Codable + Extensions

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. Decode Bool 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:

swift

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. 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. 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

swift

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. 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. 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. 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 😂

swift

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). 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()`. 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:

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:

swift

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!