Last active
January 24, 2019 14:15
-
-
Save AliSoftware/70ca140f6897ac8cea816d3985e9e1f1 to your computer and use it in GitHub Desktop.
Decode an array of objects, allowing some of the items to fail without stopping the decoding of the rest
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
//: ## Option 1: A Failable type for each item | |
//: In practice, it's very similar to the concept of the "Result" type that you see in most code bases (and that will be integrated in Swift 5), so if you already have a Result type, you might just want to use it instead, but if not, this is a super-simplification of it | |
enum Failable<T>: CustomStringConvertible { | |
case success(T) | |
case failure(Error) | |
var description: String { | |
switch self { | |
case .success(let v): return ".success(\(v))" | |
case .failure(let e): return ".failure(\(e))" | |
} | |
} | |
} | |
extension Failable { | |
// For convenience access to associated values | |
var value: T? { | |
if case .success(let v) = self { | |
return v | |
} else { | |
return nil | |
} | |
} | |
var error: Error? { | |
if case .failure(let e) = self { | |
return e | |
} else { | |
return nil | |
} | |
} | |
} | |
//: ## Make Failable Decodable-friendly | |
//: If you already have a Result type and didn't declare the Faiable type above because you wanted to use your already existing Result type instead, simply extend `Result` instead of `Failable` here. | |
extension Failable: Decodable where T: Decodable { | |
init(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() | |
do { | |
self = .success(try container.decode(T.self)) | |
} catch { | |
self = .failure(error) | |
} | |
} | |
} | |
//: ## Option 2: Use a FailableArray ignoring all errors silently | |
//: This means we'll end up with an array of our T objects — with all bad objects filtered out (instead of having an array of Failable objects each being either a success or failure) | |
struct FailableArray<T: Decodable>: Decodable { | |
let items: [T] | |
private struct EmptyDecodable: Decodable {} | |
init(from decoder: Decoder) throws { | |
var container = try decoder.unkeyedContainer() | |
var items: [T] = [] | |
while !container.isAtEnd { | |
do { | |
items.append(try container.decode(T.self)) | |
} catch { | |
// just skip the bad object and go to the next one | |
_ = try? container.decode(EmptyDecodable.self) | |
} | |
} | |
self.items = items | |
} | |
} | |
//: ## How to use in your models | |
struct Playlist: Decodable, CustomStringConvertible { | |
let id: Int | |
let name: String | |
var description: String { return "<Playlist #\(id) \(name)>" } | |
} | |
struct Track: Decodable, CustomStringConvertible { | |
let id: Int | |
let artist: String | |
let title: String | |
var description: String { return "<Track #\(id) \(title) by \(artist)>" } | |
} | |
struct APIResponse: Decodable { | |
let playlists: [Failable<Playlist>] // 1st option: a regular array where each item tells if it failed or succeeded | |
let tracks: FailableArray<Track> // 2nd option: a wrapper around an array of only the objects successfully parsed | |
} | |
//: ## Demo | |
let json = """ | |
{ | |
"playlists": [ | |
{ "id": 1, "name": "FirstPlaylist" }, | |
{ "id": 2, "title": "SecondPlaylistBadKey" }, | |
{ "id": "whoops", "name": "ThirdPlaylist" }, | |
{ "id": 4, "name": "FourthPlaylist" }, | |
], | |
"tracks": [ | |
{ "id": 1, "artist": "FirstArtist", "title": "FirstTitle" }, | |
{ "id": 2, "author": "BadAuthor", "title": "SecondTitle" }, | |
{ "id": "wat", "artist": "ThirdArtist", "title": "ThirdTitle" }, | |
{ "id": 4, "artist": "FourthArtist", "title": "FourthTitle" }, | |
] | |
} | |
""" | |
let data = json.data(using: .utf8)! | |
let decoder = JSONDecoder() | |
let response = try decoder.decode(APIResponse.self, from: data) | |
print("==== Playlists -- Each item is either a .success or a .failure") | |
response.playlists.forEach { print(" - \($0)") } | |
print("==== Tracks -- .items contains only the successfully parsed items") | |
response.tracks.items.forEach { print(" - \($0)") } | |
//: #### Result | |
/* | |
==== Playlists -- Each item is either a .success or a .failure | |
- .success(<Playlist #1 FirstPlaylist>) | |
- .failure(keyNotFound(CodingKeys(stringValue: "name", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "playlists", intValue: nil), _JSONKey(stringValue: "Index 1", intValue: 1)], debugDescription: "No value associated with key CodingKeys(stringValue: \"name\", intValue: nil) (\"name\").", underlyingError: nil))) | |
- .failure(typeMismatch(Swift.Int, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "playlists", intValue: nil), _JSONKey(stringValue: "Index 2", intValue: 2), CodingKeys(stringValue: "id", intValue: nil)], debugDescription: "Expected to decode Int but found a string/data instead.", underlyingError: nil))) | |
- .success(<Playlist #4 FourthPlaylist>) | |
==== Tracks -- .items contains only the successfully parsed items | |
- <Track #1 FirstTitle by FirstArtist> | |
- <Track #4 FourthTitle by FourthArtist> | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment