Fetch fields dynamically from API in SWIFT - arrays

I have a response from API that has a dynamic field.
[{"details": { "amount": "11"},
"wallet":"MAIN"},
{"details": { "bonus": "12"},
"wallet":"POKER"}]
I want to be able to access the ,,details`` field of each object.
I tried
if let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? Array<[String: Any]> {
completion(.success(jsonObject))}

The easiest solution here is to use Codable and to create a struct that contains a dictionary for the "dynamic" part
struct Response: Decodable {
let details: [String: String]
let wallet: String
}
and then decode it using JSONDecoder
do {
let result = try JSONDecoder().decode([Response].self, from: data)
print(result)
//...
} catch {
print(error)
}

It's better to use Codable
struct Root: Codable {
let details: Details
let wallet: String
}
// MARK: - Details
struct Details: Codable {
let amount, bonus: String?
}
let res = try? JSONDecoder().decode([Root].self,from:data)
print(res)

Related

Swift - Filter json response based on if it contains a certain key

I am wondering if I can filter a response from an API request based on if an item in the array contains a certain key.
I want to filter the array so that I only have instances of dictionaries that contain ["section": "name", "balls": 0, "bananas": 0] and have ["section_group": "group", "balls": 0, "bananas": 0] instances removed.
How would I do that in Swift?
I guess I should do something to data before I decode it here
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if (error != nil) {
print(error!)
} else {
if let data = data {
do {
let response = try JSONDecoder().decode(Response.self, from: data)
recipes = response.results
}
catch {
print(error)
}
}
}
})
Structs:
struct Response: Decodable, Encodable {
let results: [Data]
}
struct Data: Decodable, Hashable, Encodable {
let name: String
let thumbnail_url: String
let sections: [Section]
let instructions: [Instruction]
let num_servings: Int
let prep_time_minutes: Int?
let cook_time_minutes: Int?
let total_time_minutes: Int?
}
struct Section: Decodable, Hashable, Encodable {
let components: [Ingredient]
}
struct Ingredient: Decodable, Hashable, Encodable {
let raw_text: String
}
struct Instruction: Decodable, Hashable, Encodable {
let display_text: String
}
You can filter an array of dictionaries as follows:
let desired_results = all.filter{$0.keys.contains("sections")}
where all is your array of [String: Any] dictionaries.
The closure returns a boolean, true if the keys of the dictionary contain "sections".
More about filter: https://developer.apple.com/documentation/swift/sequence/3018365-filter
edit: If you want to filter Response.results to only include the 50-key objects, you can ignore ones that fail to be decoded as follows:
struct Response: Decodable, Encodable {
let results: [String]
}
I renamed your Data to Recipe because Data is already a type, and you should use a different name.
let response = try JSONDecoder().decode(Response.self, from: data)
recipes = response.results.compactMap({
do {
return try JSONDecoder().decode(Recipe.self, from: Data($0))
} catch {
return nil
}
})
compactMap ignores the nil values, which are the ones where JSONDecoder fails to decode (the objects). This way you have an array of Recipe

Problem converting JSON to Array with Swift

I try to convert a JSON to an Array but I face some issue and do not know how to sort it out.
I use Swift 5 and Xcode 12.2.
Here is the JSON from my PHP query:
[
{
Crewcode = AAA;
Phone = 5553216789;
Firstname = Philip;
Lastname = MILLER;
email = "pmiller#xxx.com";
},
{
Crewcode = BBB;
Phone = 5557861243;
Firstname = Andrew;
Lastname = DEAN;
email = "adean#xxx.com";
}
]
And here is my Swift code :
let url: URL = URL(string: "https://xxx.php")!
let defaultSession = Foundation.URLSession(configuration: URLSessionConfiguration.default)
let task = defaultSession.dataTask(with: url) { (data, response, error) in
if error != nil {
print("Failed to download data")
}
else {
print("Data downloaded")
do {
if let jsondata = (try? JSONSerialization.jsonObject(with: data!, options: [])) {
print(jsondata)
struct Crew: Decodable {
var Code: String
var Phone: String
var Firstname: String
var Lastname: String
var email: String
}
let decoder = JSONDecoder()
do {
let people = try decoder.decode([Crew].self, from: jsondata as! Data)
print(people)
}
catch {
print(error)
}
}
}
}
}
task.resume()
When I run my code I get the following error:
Could not cast value of type '__NSArrayI' (0x7fff86b930b0) to 'NSData' (0x7fff86b911e8).
2020-12-09 14:52:48.988468+0100 FTL[57659:3019805] Could not cast value of type '__NSArrayI' (0x7fff86b930b0) to 'NSData' (0x7fff86b911e8).
Could not cast value of type '__NSArrayI' (0x7fff86b930b0) to 'NSData' (0x7fff86b911e8).
Should you have any idea to get it right, I thank you in advance for your assistance on this !
You are deserializing the JSON twice by mixing up JSONSerialization and JSONDecoder.
Delete the first one
if let jsondata = (try? JSONSerialization.jsonObject(with: data!, options: [])) {
– By the way the JSON in the question is neither fish nor fowl, neither JSON nor an NS.. collection type dump –
and replace
let people = try decoder.decode([Crew].self, from: jsondata as! Data)
with
let people = try decoder.decode([Crew].self, from: data!)
and the struct member names must match the keys otherwise you have to add CodingKeys

parsing array of objects to array of string elements in swift JSON object

I am trying to parse the JSON object received from Web service which gives the result as JSON object of status and data.data again is a array of objects, from this object I want to take one element on the basis of which I have to fill a tableview.
web Service results comes as
{"status":1,"data":[{"service_id":"1","service_name":"Painter"},{"service_id":"2","service_name":"Plumber"},{"service_id":"3","service_name":"Electrician"},{"service_id":"4","service_name":"Handyman"},{"service_id":"5","service_name":"Carpenter"},{"service_id":"6","service_name":"Mason"}]}
parsing in swift I did as:--
created one class
class ABC: NSObject {
var service_name:String?
var service_id : Int?
init(service_name:String,service_id:Int) {
self.service_name = service_name
self.service_id = service_id
}
let myUrl = URL(string: "services.php");
var request = URLRequest(url:myUrl!)
request.httpMethod = "POST"// Compose a query string
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
if error != nil
{
print("error=\(String(describing: error))")
return
}
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSDictionary
if let parseJSON = json
{
let status=parseJSON["status"] as? Int
let newdata : NSDictionary = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as! NSDictionary
self.model=(newdata.value(forKey: "data") as? [ABC])!
My problem is I am getting an array of objects in self.model as service_name and service_id keys.Now I want to take out one array of strings that contains all the service_name object values.Its saying not able to convert NSArray to swift array.
As already mentioned by others use (De)codable. It's easy to use and very comfortable. No type cast and no literal keys.
Create two structs, declare the members as non-optional constants with camelCased names and omit the initializer.
struct Root : Decodable {
let status : Int
let data : [Service]
}
struct Service : Decodable {
let serviceName : String
let serviceId : String
}
Then decode the JSON in another class or struct
let myUrl = URL(string: "services.php")
var request = URLRequest(url: myUrl!)
request.httpMethod = "POST"// Compose a query string
let task = URLSession.shared.dataTask(with: request) { data, _, error in
if let error = error { print(error); return }
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let json = try decoder.decode(Root.self, from: data!)
let status = json.status
let newdata = json.data
} catch { print(error))
}
task.resume()
I would recommend to drop JSONSerialization and use Codable protocol instead with CodingKeys.
Here is a simple example to see how it works.
struct Service : Codable {
let id : Int
let name : String
// keys
private enum CodingKeys: String, CodingKey {
case id = "service_id"
case name = "service_name"
}
}
...
// assuming our data comes from server side
let jsonString = "..."
let jsonData = jsonString.data(using: .utf8)!
do {
let jsonDecoder = JSONDecoder()
let user = try jsonDecoder.decode(Service.self, from: jsonData)
print("Getting service: \(service.id) \(service.name)")
} catch {
print("Unexpected error: \(error).")
}
More documentation here.
Use native Swift type Dictionary everywhere you use NSDictionary now
Then get certain value for key by specifing key in dictionary's subscript
if let model = newData["data"] as? [ABC] {
self.model = model
}
Anyway, I would suggest you to start using Codable instead of JSONSerialization
struct Response: Decodable {
let status: Int
let data: [ABC]
}
struct ABC: Decodable {
let serviceName: String
let serviceId: String // note that `serviceId` isn’t `Int` But `String`
}
and then decode your data using JSONDecoder
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let response = try decoder.decode(Response.self, from: data!)
self.model = response.data
} catch { print(error) }

Swift 4 : Decode array

I want to easily decode a JSON file with Decode protocol from Swift 4 on Xcode 9. This my question :
How to decode à JSON like this:
[
{
"name": "My first Catalog",
"order": 0,
"products": [
{
"product": {
"title": "product title",
"reference": "ref"
}
}
]
}
]
I try this but it's doesn't work
fileprivate struct Catalog: Codable {
var name: String
var order: Int
var product: [Product]
}
fileprivate struct Product: Codable {
var title: String
var reference: String
}
...
// JSON Decoder
do {
let jsonData = try Data(contentsOf: URL(fileURLWithPath: filePath), options: .alwaysMapped)
let jsonDecoder = JSONDecoder()
let jsonCatalogs = try? jsonDecoder.decode(Array<Catalog>.self,
from: jsonData)
return jsonCatalogs
} catch {
print ("error")
return nil
}
I don't know why it doesn't work in Swift 4 with Xcode 9. Thank for your help ;-)
actually the thing is that your structs are wrong,
fileprivate struct Catalog: Codable {
var name: String
var order: Int
var products: [Products] // this key is wrong in your question, it should be products instead of product
}
//this particular structure was missing as your products is having a dictionary and in that dictionary you are having product at key product
fileprivate struct Products: Codable {
var product: Product
}
fileprivate struct Product: Codable {
var title: String
var reference: String
}
now you can check your fucntion and also you can easily debug it using try catch with error handlings
...
// JSON Decoder
do {
let jsonData = try Data(contentsOf: URL(fileURLWithPath:filePath), options: .alwaysMapped)
let jsonDecoder = JSONDecoder()
let jsonCatalogs = try? jsonDecoder.decode(Array<Catalog>.self,from: jsonData)
print(jsonCatalogs)
return jsonCatalogs
} catch let error {
print ("error -> \(error)") // this will always give you the exact reason for which you are getting error
return nil
}
Try this solution according to your JSON and i have worked with Decoder in swift 4 with xcode 9.4.
struct Catalog : Decodable {
let name : String?
let order : Int?
let productArray : [Products]? // this is products array.
}
struct Products : Decodable {
let productDict : Product? // this is product dictionary
}
struct Product : Decodable {
let title : String?
let reference : String?
}
var catalogArray = [Catalog]() // Declaration
// JSON Decoder
do {
let jsonData = try Data(contentsOf: URL(fileURLWithPath:filePath), options: .alwaysMapped)
let jsonDecoder = JSONDecoder()
let jsonCatalogs = try? jsonDecoder.decode(Catalog.self,from: jsonData)
return jsonCatalogs
} catch let error {
print ("error -> \(error)") // this will always give you the exact reason for which you are getting error
return nil
}
*

Swift JSONDecode decoding arrays fails if single element decoding fails

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")]

Resources