I'm having difficulty trying to figure out how to loop TMDb results from my publisher 1 and use the value I'm getting from key id for my second publisher. It's not looping through each result, it does get the proper URL inside my func fetchVideos(_ id: Int) but it's not calling each URL from TMDb's results array. I'm not sure if it's because I'm also getting an Array of results from Videos Codable data?
I attempted to use Publishers.MergeMany in my first publisher's flatMap. I'm still definitely at a novice level for combine, any tips would help. I'm trying to get a list of movies, then from the movies get the id key then use that to fetch the movie trailer data for each movie.
print output
https://api.themoviedb.org/3/movie/602223/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/459151/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/385128/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/522478/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/637693/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/529203/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/578701/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/631843/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/645856/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/581644/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/436969/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/568620/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/522931/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/681260/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/630586/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/671/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/618416/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/646207/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/550988/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/482373/videos?api_key=API_KEY&language=en-US
Videos(id: 459151, results: [themoviedb_demo.Video(id: 3F24D610-4261-4AEB-8906-A9D0E5FE8E4D, iso639_1: "en", iso3166_1: "US", key: "CK6xdYIsaa0", name: "DreamWorks\' The Boss Baby: Family Business | Official Trailer #3 | Peacock", site: "YouTube", size: 1080, type: "Trailer"), themoviedb_demo.Video(id: 417EF49C-B983-4CC3-B435-7A902DECE917, iso639_1: "en", iso3166_1: "US", key: "-rF2j6K5FoM", name: "The Boss Baby 2: Family Business – Official Trailer 2 (Universal Pictures) HD", site: "YouTube", size: 1080, type: "Trailer"), themoviedb_demo.Video(id: C34A3F5F-9429-4267-86F0-5506EF3E8281, iso639_1: "en", iso3166_1: "US", key: "QPzy8Ckza08", name: "THE BOSS BABY: FAMILY BUSINESS | Official Trailer", site: "YouTube", size: 1080, type: "Trailer")])
Codable data
struct Videos: Codable {
let id: Int
let results: [Video]
}
struct Video: Codable {
let id = Int
let key: String
let name: String
enum CodingKeys: String, CodingKey {
case id
case key, name
}
}
struct TMDb: Codable {
let results: [Results]?
}
struct Results: Codable {
let id: Int
let releaseDate, title: String?
let name: String?
enum CodingKeys: String, CodingKey {
case id
case releaseDate = "release_date"
case title
case name
}
}
#Published var movies = TMDb(results: Array(repeating: Results(id: 1, releaseDate: "", title: "", name: "") , count: 5))
#Published var videos = Videos(id: 1, results: Array(repeating: Video(id: 1, key: "", name: "") , count: 5))
func getUpcoming() {
var request = URLRequest(url:URL(string:"https://api.themoviedb.org/3/movie/upcoming?api_key=API_KEY&language=en-US&page=1")!)
request.httpMethod = "GET"
let publisher = URLSession.shared.dataTaskPublisher(for: request)
.map{ $0.data }
.decode(type: TMDb.self, decoder: JSONDecoder())
let publisher2 = publisher
.flatMap{
// loop results from TMDb for id for publisher 2, only one is called
Publishers.MergeMany($0.results!.map { item in
return self.fetchVideos(item.id)
.map { $0 as Videos }
.replaceError(with: nil)
})
}
// Publishers.CombineLatest
Publishers.Zip(publisher, publisher2)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: {_ in
}, receiveValue: { movies, videos in
self.movies = movies
self.videos = videos
}).store(in: &cancellables)
}
func fetchVideos(_ id: Int) -> AnyPublisher<Videos, Error> {
let url = URL(string: "https://api.themoviedb.org/3/movie/\(id)/videos?api_key=API_KEY&language=en-US")!
return URLSession.shared.dataTaskPublisher(for: url)
.mapError { $0 as Error }
.map{ $0.data }
.decode(type: Videos.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
Hi #cole for this operation you don't need either the merge or the zip, because you are not subscribing to two publishers, you are attempting to do an action after your first publisher emitted an event.
For this you only need a map .handleEvents in my opinion.
So lets try to enhance your code, we want to update both movies and videos separately, but we still need videos to be dependent of movies
First we will create the publisher to request the movies:
var request = URLRequest(url:URL(string:"https://api.themoviedb.org/3/movie/upcoming?api_key=API_KEY&language=en-US&page=1")!)
request.httpMethod = "GET"
let publisher = URLSession.shared.dataTaskPublisher(for: request)
.map{ $0.data }
.decode(type: TMDb.self, decoder: JSONDecoder())
Now we enhance this publisher handling by assigning movies:
var request = URLRequest(url:URL(string:"https://api.themoviedb.org/3/movie/upcoming?api_key=API_KEY&language=en-US&page=1")!)
request.httpMethod = "GET"
let publisher = URLSession.shared.dataTaskPublisher(for: request)
.map{ $0.data }
.decode(type: TMDb.self, decoder: JSONDecoder())
.sink(receiveCompletion: { print ($0) },
receiveValue: { self.movies = $0.results })
Now we will add .handleEvent in order to iterate through our movies to create all the publishers which emit videos events and append videos for the videos array:
var request = URLRequest(url:URL(string:"https://api.themoviedb.org/3/movie/upcoming?api_key=API_KEY&language=en-US&page=1")!)
request.httpMethod = "GET"
let publisher = URLSession.shared.dataTaskPublisher(for: request)
.map{ $0.data }
.decode(type: TMDb.self, decoder: JSONDecoder())
.sink(receiveCompletion: { print ($0) },
receiveValue: { self.movies = $0.results })
.handleEvents(receiveSubscription:nil, receiveOutput: { [weak self] movies in guard let self = self else {return}
self.videos = [Videos]()
for movie in movies.results {
self.fetchVideos(movie.id)
}, receiveCompletion:nil, receiveCancel:nil, receiveRequest:nil)
})
.store(in: &cancellables)
Now for the last step lets update the fetchVideos accordingly:
func fetchVideos(_ id: Int) {
let url = URL(string: "https://api.themoviedb.org/3/movie/\(id)/videos?api_key=API_KEY&language=en-US")!
return URLSession.shared.dataTaskPublisher(for: url)
.mapError { $0 as Error }
.map{ $0.data }
.decode(type: Videos.self, decoder: JSONDecoder())
.sink(receiveCompletion: { print ($0) },
receiveValue: { [weak self] videos in guard let self = self else {return}
self.videos.append(videos)
})
.store(in: &cancellables)
}
Solved my own question. I needed to create an array for my #Published variable instead of single item. Then I needed to call .collect() and .append() in my publisher's flatMap which will append to my #Published variable videos.
I'm having trouble getting a function call using a generic type to work using a URLSession DataTaskPublisher...
The API I'm calling always responds with an HTTP 200, and indicates whether it was successful or not in the JSON via a code string with an indicator.
I created a protocol that all response objects conform to, e.g.:
protocol APIResponse: Decodable {
var code: String { get }
}
My actual response will be something like:
struct LoginResponse : APIResponse {
let code: String
let name: String
enum CodingKeys: String, CodingKey {
case code = "code"
case name = "name"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
code = try values.decode(String.self, forKey: .code)
name = try values.decode(String.self, forKey: .name)
}
}
Now I want a function like this that I can use:
private func call<T: APIResponse>(_ endpoint: String, using data: Encodable, providing response: T)
-> AnyPublisher<T, Error> {
let request = createRequest(for: endpoint, using: data)
return session
.dataTaskPublisher(for: request)
.map {
$0.data
}
.decode(type: T.self, decoder: decoder)
.tryMap {
response in
if response.code.suffix(1) != "I" {
throw MyError(message: "Error \(code)")
}
return response
}
.eraseToAnyPublisher()
}
So far, so good!
But here's the problem... I want to use it like this:
func login() -> AnyPublisher<String, Error> {
call("login", using: LoginRequest(), providing: LoginResponse)
.map {
$0.name
}
.eraseToAnyPublisher()
}
The compiler complains with Type 'LoginResponse.Type' cannot conform to 'APIResponse'; only struct/enum/class types can conform to protocols
My fix (which works, but is kludgy), it to provide a no-arg init() for LoginResponse and call it like this: call("login", using: LoginRequest(), providing: LoginResponse())
Any way to get the Generic to work without the no-arg init()?
Should I be taking a totally different approach?
In the header for call change response parameter to
...providing response: T.Type)
and call it using .self
call("login", using: "request", providing: LoginResponse.self)
You don't really need to pass the response type to this function:
private func call<T: APIResponse>(_ endpoint: String,
using data: Encodable) -> AnyPublisher<T, Error> {
// no change here
}
But you might have to help the compiler here by specifying the type of the closure used in map as it has no way of guessing which type is returned from this generic function:
func login() -> AnyPublisher<String, Error> {
call("login", using: LoginRequest())
.map { (response: LoginResponse) -> String in
response.name
}
.eraseToAnyPublisher()
}
You can also simplify your LoginResponse:
struct LoginResponse: APIResponse {
let code: String
let name: String
}
As an alternative, the cleanest approach, in my view, is to create an Endpoint protocol that knows about its own response, so as to remove this redundancy from the call function.
It would look something like this:
protocol Endpoint {
associatedtype Response: Decodable
var request: URLRequest { get }
}
func call<E: Endpoint>(endpoint: E) -> AnyPublisher<E.Response, Error> {
//...
}
define the endpoint like this:
struct LoginResponse: Decodable {
// ...
}
struct LoginEndpoint: Endpoint {
typealias Response = LoginResponse
let request: URLRequest
}
and use it:
let login = LoginEndpoint(request: URLRequest(...))
call(endpoint: login)
.sink ...
I am having trouble compiling the following code — it fails with “'Cannot convert value of type 'CheckUserNameAvailability' to closure result type 'JSONDecoder.Input' (aka 'Data')”.
How do I map the CheckUserNameAvailability JSON returned by the server to Data?
// This is the JSON returned by the server
// {
// "Username" : "Foo1Bar2",
// "Available" : true
// }
struct CheckUserNameAvailability: Codable {
let Username: String
let Available: Bool
}
enum tvAPI {
static func checkUserName(username: String) -> AnyPublisher<CheckUserNameAvailability, Error> {
// ...
}
}
func testUserName() {
var cancellable: AnyCancellable?
let me = "Foo1Bar2"
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .useDefaultKeys
cancellable = tvAPI.checkUserName(username: me)
.map { $0 } // compilation fails with “Cannot convert value of type 'CheckUserNameAvailability' to closure result type 'JSONDecoder.Input' (aka 'Data')”
.decode(type: [CheckUserNameAvailability].self, decoder: JSONDecoder())
// .print()
.sink(receiveCompletion: {completion in
print(completion)
},
receiveValue: { availability in
print("\(availability)")
})
RunLoop.main.run(until: Date(timeIntervalSinceNow: 10))
withExtendedLifetime(cancellable, {})
}
tvAPI.checkUserName(username:) returns a publisher with output type CheckUserNameAvailability. The decode method on the publisher requires that the publisher's output matches the decoder's input:
func decode<Item, Coder>(type: Item.Type, decoder: Coder) ->
Publishers.Decode<Self, Item, Coder> where
Item : Decodable,
Coder : TopLevelDecoder,
Self.Output == Coder.Input
However, CheckUserNameAvailability is not compatible with JSONDecoder.Input (aka Data).
You don't need to decode the data:
cancellable = tvAPI.checkUserName(username: me)
// .print()
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { availability in
print("\(availability)")
})
Not sure what to do where; I believe my type is conforming to decodable!
let dm = DataManager(networkManagers: [mockHTTPManager])
let ep = Endpoint(scheme: .http, host: "api.nytimes.com",path: "/search/repositories")
dm.object(from: ep, with: DisplayContent.self) {result in
print (result)
}
where DisplayContent is a struct
struct DisplayContent:Decodable {
var title: String?
var abstract: String?
var thumbnailImageString: String?
var date: String?
var image: String?
}
and I'm trying to create a method to generically convert data to an object, but I think just the singature is relevant here
func object<T : Decodable>(from endpoint: Endpoint, with object: T, completion: #escaping (Result<T, Error>) -> Void) {
let error = NSError(domain:"", code:-1009, userInfo:[ NSLocalizedDescriptionKey: "Internet Offline"]) as Error
let url = endpoint.url!
networkManagers.first!.get(url: url) { result in
switch result {
case .failure: print ("failure")
case .success(let success) :
do {
let decoder = JSONDecoder()
let content = try decoder.decode(T.self, from: success)
print ("content")
} catch {
}
}
}
}
The error is "Argument type 'DisplayContent.Type' does not conform to expected type 'Decodable'" yet DisplayContent conforms to decodatble!
Your function is expecting an object of type T (with object: T) but you are trying to pass a type (DisplayContent.self).
You can either:
Pass an object of the correct type (e.g. DisplayContent()), or
Tell the function to expect a type to be passed (e.g. with object: T.type).
While using Swift4 and Codable protocols I got the following problem - it looks like there is no way to allow JSONDecoder to skip elements in an array.
For example, I have the following JSON:
[
{
"name": "Banana",
"points": 200,
"description": "A banana grown in Ecuador."
},
{
"name": "Orange"
}
]
And a Codable struct:
struct GroceryProduct: Codable {
var name: String
var points: Int
var description: String?
}
When decoding this json
let decoder = JSONDecoder()
let products = try decoder.decode([GroceryProduct].self, from: json)
Resulting products is empty. Which is to be expected, due to the fact that the second object in JSON has no "points" key, while points is not optional in GroceryProduct struct.
Question is how can I allow JSONDecoder to "skip" invalid object?
One option is to use a wrapper type that attempts to decode a given value; storing nil if unsuccessful:
struct FailableDecodable<Base : Decodable> : Decodable {
let base: Base?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.base = try? container.decode(Base.self)
}
}
We can then decode an array of these, with your GroceryProduct filling in the Base placeholder:
import Foundation
let json = """
[
{
"name": "Banana",
"points": 200,
"description": "A banana grown in Ecuador."
},
{
"name": "Orange"
}
]
""".data(using: .utf8)!
struct GroceryProduct : Codable {
var name: String
var points: Int
var description: String?
}
let products = try JSONDecoder()
.decode([FailableDecodable<GroceryProduct>].self, from: json)
.compactMap { $0.base } // .flatMap in Swift 4.0
print(products)
// [
// GroceryProduct(
// name: "Banana", points: 200,
// description: Optional("A banana grown in Ecuador.")
// )
// ]
We're then using .compactMap { $0.base } to filter out nil elements (those that threw an error on decoding).
This will create an intermediate array of [FailableDecodable<GroceryProduct>], which shouldn't be an issue; however if you wish to avoid it, you could always create another wrapper type that decodes and unwraps each element from an unkeyed container:
struct FailableCodableArray<Element : Codable> : Codable {
var elements: [Element]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var elements = [Element]()
if let count = container.count {
elements.reserveCapacity(count)
}
while !container.isAtEnd {
if let element = try container
.decode(FailableDecodable<Element>.self).base {
elements.append(element)
}
}
self.elements = elements
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(elements)
}
}
You would then decode as:
let products = try JSONDecoder()
.decode(FailableCodableArray<GroceryProduct>.self, from: json)
.elements
print(products)
// [
// GroceryProduct(
// name: "Banana", points: 200,
// description: Optional("A banana grown in Ecuador.")
// )
// ]
I would create a new type Throwable, which can wrap any type conforming to Decodable:
enum Throwable<T: Decodable>: Decodable {
case success(T)
case failure(Error)
init(from decoder: Decoder) throws {
do {
let decoded = try T(from: decoder)
self = .success(decoded)
} catch let error {
self = .failure(error)
}
}
}
For decoding an array of GroceryProduct (or any other Collection):
let decoder = JSONDecoder()
let throwables = try decoder.decode([Throwable<GroceryProduct>].self, from: json)
let products = throwables.compactMap { $0.value }
where value is a computed property introduced in an extension on Throwable:
extension Throwable {
var value: T? {
switch self {
case .failure(_):
return nil
case .success(let value):
return value
}
}
}
I would opt for using a enum wrapper type (over a Struct) because it may be useful to keep track of the errors that are thrown as well as their indices.
Swift 5
For Swift 5 Consider using the Result enum e.g.
struct Throwable<T: Decodable>: Decodable {
let result: Result<T, Error>
init(from decoder: Decoder) throws {
result = Result(catching: { try T(from: decoder) })
}
}
To unwrap the decoded value use the get() method on the result property:
let products = throwables.compactMap { try? $0.result.get() }
The problem is that when iterating over a container, the container.currentIndex isn’t incremented so you can try to decode again with a different type.
Because the currentIndex is read only, a solution is to increment it yourself successfully decoding a dummy. I took #Hamish solution, and wrote a wrapper with a custom init.
This problem is a current Swift bug: https://bugs.swift.org/browse/SR-5953
The solution posted here is a workaround in one of the comments.
I like this option because I’m parsing a bunch of models the same way on a network client, and I wanted the solution to be local to one of the objects. That is, I still want the others to be discarded.
I explain better in my github https://github.com/phynet/Lossy-array-decode-swift4
import Foundation
let json = """
[
{
"name": "Banana",
"points": 200,
"description": "A banana grown in Ecuador."
},
{
"name": "Orange"
}
]
""".data(using: .utf8)!
private struct DummyCodable: Codable {}
struct Groceries: Codable
{
var groceries: [GroceryProduct]
init(from decoder: Decoder) throws {
var groceries = [GroceryProduct]()
var container = try decoder.unkeyedContainer()
while !container.isAtEnd {
if let route = try? container.decode(GroceryProduct.self) {
groceries.append(route)
} else {
_ = try? container.decode(DummyCodable.self) // <-- TRICK
}
}
self.groceries = groceries
}
}
struct GroceryProduct: Codable {
var name: String
var points: Int
var description: String?
}
let products = try JSONDecoder().decode(Groceries.self, from: json)
print(products)
There are two options:
Declare all members of the struct as optional whose keys can be missing
struct GroceryProduct: Codable {
var name: String
var points : Int?
var description: String?
}
Write a custom initializer to assign default values in the nil case.
struct GroceryProduct: Codable {
var name: String
var points : Int
var description: String
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
points = try values.decodeIfPresent(Int.self, forKey: .points) ?? 0
description = try values.decodeIfPresent(String.self, forKey: .description) ?? ""
}
}
A solution made possible by Swift 5.1, using the property wrapper:
#propertyWrapper
struct IgnoreFailure<Value: Decodable>: Decodable {
var wrappedValue: [Value] = []
private struct _None: Decodable {}
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
while !container.isAtEnd {
if let decoded = try? container.decode(Value.self) {
wrappedValue.append(decoded)
}
else {
// item is silently ignored.
try? container.decode(_None.self)
}
}
}
}
And then the usage:
let json = """
{
"products": [
{
"name": "Banana",
"points": 200,
"description": "A banana grown in Ecuador."
},
{
"name": "Orange"
}
]
}
""".data(using: .utf8)!
struct GroceryProduct: Decodable {
var name: String
var points: Int
var description: String?
}
struct ProductResponse: Decodable {
#IgnoreFailure
var products: [GroceryProduct]
}
let response = try! JSONDecoder().decode(ProductResponse.self, from: json)
print(response.products) // Only contains banana.
Note: The property wrapper things will only works if the response can be wrapped in a struct (i.e: not a top level array).
In that case, you can still wrap it manually (with a typealias for better readability):
typealias ArrayIgnoringFailure<Value: Decodable> = IgnoreFailure<Value>
let response = try! JSONDecoder().decode(ArrayIgnoringFailure<GroceryProduct>.self, from: json)
print(response.wrappedValue) // Only contains banana.
Ive put #sophy-swicz solution, with some modifications, into an easy to use extension
fileprivate struct DummyCodable: Codable {}
extension UnkeyedDecodingContainer {
public mutating func decodeArray<T>(_ type: T.Type) throws -> [T] where T : Decodable {
var array = [T]()
while !self.isAtEnd {
do {
let item = try self.decode(T.self)
array.append(item)
} catch let error {
print("error: \(error)")
// hack to increment currentIndex
_ = try self.decode(DummyCodable.self)
}
}
return array
}
}
extension KeyedDecodingContainerProtocol {
public func decodeArray<T>(_ type: T.Type, forKey key: Self.Key) throws -> [T] where T : Decodable {
var unkeyedContainer = try self.nestedUnkeyedContainer(forKey: key)
return try unkeyedContainer.decodeArray(type)
}
}
Just call it like this
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.items = try container.decodeArray(ItemType.self, forKey: . items)
}
For the example above:
let json = """
[
{
"name": "Banana",
"points": 200,
"description": "A banana grown in Ecuador."
},
{
"name": "Orange"
}
]
""".data(using: .utf8)!
struct Groceries: Codable
{
var groceries: [GroceryProduct]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
groceries = try container.decodeArray(GroceryProduct.self)
}
}
struct GroceryProduct: Codable {
var name: String
var points: Int
var description: String?
}
let products = try JSONDecoder().decode(Groceries.self, from: json)
print(products)
Instead, You can also do like this:
struct GroceryProduct: Decodable {
var name: String
var points: Int
var description: String?
}'
and then in while getting it:
'let groceryList = try JSONDecoder().decode(Array<GroceryProduct>.self, from: responseData)'
Unfortunately Swift 4 API doesn't have failable initializer for init(from: Decoder).
Only one solution that I see is implementing custom decoding, giving default value for optional fields and possible filter with needed data:
struct GroceryProduct: Codable {
let name: String
let points: Int?
let description: String
private enum CodingKeys: String, CodingKey {
case name, points, description
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
points = try? container.decode(Int.self, forKey: .points)
description = (try? container.decode(String.self, forKey: .description)) ?? "No description"
}
}
// for test
let dict = [["name": "Banana", "points": 100], ["name": "Nut", "description": "Woof"]]
if let data = try? JSONSerialization.data(withJSONObject: dict, options: []) {
let decoder = JSONDecoder()
let result = try? decoder.decode([GroceryProduct].self, from: data)
print("rawResult: \(result)")
let clearedResult = result?.filter { $0.points != nil }
print("clearedResult: \(clearedResult)")
}
I improved on #Hamish's for the case, that you want this behaviour for all arrays:
private struct OptionalContainer<Base: Codable>: Codable {
let base: Base?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
base = try? container.decode(Base.self)
}
}
private struct OptionalArray<Base: Codable>: Codable {
let result: [Base]
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let tmp = try container.decode([OptionalContainer<Base>].self)
result = tmp.compactMap { $0.base }
}
}
extension Array where Element: Codable {
init(from decoder: Decoder) throws {
let optionalArray = try OptionalArray<Element>(from: decoder)
self = optionalArray.result
}
}
Swift 5
Inspired with previous answers I decode inside Result enum extension.
What do you think about it?
extension Result: Decodable where Success: Decodable, Failure == DecodingError {
public init(from decoder: Decoder) throws {
let container: SingleValueDecodingContainer = try decoder.singleValueContainer()
do {
self = .success(try container.decode(Success.self))
} catch {
if let decodingError = error as? DecodingError {
self = .failure(decodingError)
} else {
self = .failure(DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: error.localizedDescription)))
}
}
}
}
Usage
let listResult = try? JSONDecoder().decode([Result<SomeObject, DecodingError>].self, from: ##YOUR DATA##)
let list: [SomeObject] = listResult.compactMap {try? $0.get()}
#Hamish's answer is great. However, you can reduce FailableCodableArray to:
struct FailableCodableArray<Element : Codable> : Codable {
var elements: [Element]
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let elements = try container.decode([FailableDecodable<Element>].self)
self.elements = elements.compactMap { $0.wrapped }
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(elements)
}
}
I had a similar issue recently, but slightly different.
struct Person: Codable {
var name: String
var age: Int
var description: String?
var friendnamesArray:[String]?
}
In this case, if one of the element in friendnamesArray is nil, the whole object is nil while decoding.
And the right way to handle this edge case is to declare the string array[String] as array of optional strings[String?] as below,
struct Person: Codable {
var name: String
var age: Int
var description: String?
var friendnamesArray:[String?]?
}
You made the description optional, you should also make the points field optional if there is a chance it could be nil, such as this:
struct GroceryProduct: Codable {
var name: String
var points: Int?
var description: String?
}
Just make sure you safe-unwrap it however you see fit for it's use. I'm guessing nil points == 0 in the actual use case so an example could be:
let products = try JSONDecoder().decode([GroceryProduct].self, from: json)
for product in products {
let name = product.name
let points = product.points ?? 0
let description = product.description ?? ""
ProductView(name, points, description)
}
or in-line:
let products = try JSONDecoder().decode([GroceryProduct].self, from: json)
for product in products {
ProductView(product.name, product.points ?? 0, product.description ?? "")
}
I come up with this KeyedDecodingContainer.safelyDecodeArray that provides a simple interface:
extension KeyedDecodingContainer {
/// The sole purpose of this `EmptyDecodable` is allowing decoder to skip an element that cannot be decoded.
private struct EmptyDecodable: Decodable {}
/// Return successfully decoded elements even if some of the element fails to decode.
func safelyDecodeArray<T: Decodable>(of type: T.Type, forKey key: KeyedDecodingContainer.Key) -> [T] {
guard var container = try? nestedUnkeyedContainer(forKey: key) else {
return []
}
var elements = [T]()
elements.reserveCapacity(container.count ?? 0)
while !container.isAtEnd {
/*
Note:
When decoding an element fails, the decoder does not move on the next element upon failure, so that we can retry the same element again
by other means. However, this behavior potentially keeps `while !container.isAtEnd` looping forever, and Apple does not offer a `.skipFailable`
decoder option yet. As a result, `catch` needs to manually skip the failed element by decoding it into an `EmptyDecodable` that always succeed.
See the Swift ticket https://bugs.swift.org/browse/SR-5953.
*/
do {
elements.append(try container.decode(T.self))
} catch {
if let decodingError = error as? DecodingError {
Logger.error("\(#function): skipping one element: \(decodingError)")
} else {
Logger.error("\(#function): skipping one element: \(error)")
}
_ = try? container.decode(EmptyDecodable.self) // skip the current element by decoding it into an empty `Decodable`
}
}
return elements
}
}
The potentially infinite loop while !container.isAtEnd is a concern, and it's addressed by using EmptyDecodable.
A much simpler attempt:
Why don't you declare points as optional or make the array contain optional elements
let products = [GroceryProduct?]
Features:
Simple use. One line in Decodable instance: let array: CompactDecodableArray<Int>
Is decoded with standard mapping mechanism: JSONDecoder().decode(Model.self, from: data)
skips incorrect elements (returns array with only successful mapped elements)
Details
Xcode 12.1 (12A7403)
Swift 5.3
Solution
class CompactDecodableArray<Element>: Decodable where Element: Decodable {
private(set) var elements = [Element]()
required init(from decoder: Decoder) throws {
guard var unkeyedContainer = try? decoder.unkeyedContainer() else { return }
while !unkeyedContainer.isAtEnd {
if let value = try? unkeyedContainer.decode(Element.self) {
elements.append(value)
} else {
unkeyedContainer.skip()
}
}
}
}
// https://forums.swift.org/t/pitch-unkeyeddecodingcontainer-movenext-to-skip-items-in-deserialization/22151/17
struct Empty: Decodable { }
extension UnkeyedDecodingContainer {
mutating func skip() { _ = try? decode(Empty.self) }
}
Usage
struct Model2: Decodable {
let num: Int
let str: String
}
struct Model: Decodable {
let num: Int
let str: String
let array1: CompactDecodableArray<Int>
let array2: CompactDecodableArray<Int>?
let array4: CompactDecodableArray<Model2>
}
let dictionary: [String : Any] = ["num": 1, "str": "blablabla",
"array1": [1,2,3],
"array3": [1,nil,3],
"array4": [["num": 1, "str": "a"], ["num": 2]]
]
let data = try! JSONSerialization.data(withJSONObject: dictionary)
let object = try JSONDecoder().decode(Model.self, from: data)
print("1. \(object.array1.elements)")
print("2. \(object.array2?.elements)")
print("3. \(object.array4.elements)")
Console
1. [1, 2, 3]
2. nil
3. [__lldb_expr_25.Model2(num: 1, str: "a")]