Swift Vapor wrong decoding of array of objects - arrays

I'm trying to pass an array of objects inside another object
struct CreationData: Encodable {
enum CodingKeys: String, CodingKey {
case property1
case amount
case participants
case code
}
var property1: String?
var amount: Double?
var tipAmount: Double?
var participants: [Participant]
var currency: Currency
init() {
name = nil
property1 = nil
participants = []
tipAmount = nil
currency = .hryvna
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if let name = name {
try container.encode(name, forKey: .name)
}
if let property1 = property1 {
try container.encode(property1, forKey: .property1)
}
if let tipAmount = tipAmount {
try container.encode(tipAmount, forKey: .tipAmount)
}
if !participants.isEmpty {
try container.encode(participants, forKey: .participants)
}
try container.encode(currency, forKey: .currencyCode)
}
}
where object in array is
struct Participant: Hashable, Encodable {
enum CodingKeys: String, CodingKey {
case amount
case id
}
var image: Data?
var displayName: String
var amount: Double? = 100
var id: Int?
var name: String?
var isSelected: Bool = false
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(amount, forKey: .amount)
try container.encode(id, forKey: .id)
}
}
so my JSON looks something like this
{
"property1" : 123,
"code" : "zen",
"name" : "No name",
"participants" : [
{
"amount" : 100,
"id" : 1
}
]
}
On the backend I use this handler
func createHandler(_ req: Request, splitCreate: Create) throws -> Future<SplitResponse> {
let logger = try req.make(Logger.self)
let user = try req.requireAuthenticated(User.self)
guard let id = user.id else {
throw Abort(.badRequest, reason: "Couldn't get an id for current user")
}
...
with this request content object
struct Create: Content {
struct Participant: Content {
var id: Int?
var amount: Double?
}
var amount: Double?
var tipAmount: Double?
var name: String?
var participants: [Participant]
var currencyCode: String
}
I create pivots by the participants array, and the problem is that instead of getting a single participant from an array like so
[App.Create.Participant(id: Optional(1), amount: Optional(100.0))]
on the backend from the specified JSON I get these participants
[App.Create.Participant(id: Optional(1), amount: nil), App.Create.Participant(id: Optional(1), amount: Optional(100.0))]
Seems like my array json is being decoded incorrectly, but I'm unable to find a way to fix it

For some reason adding Content-Type header to URLSessionConfiguration.additionalHeaders didn't change the encoding to JSON.
So I had to set Alamofire's request encoding to JSONEncoding.default explicitly.

Related

Appending array of map from model in firestore document in Swift

I am trying to add to an array of maps in Firestore and getting the following error:
Terminating app due to uncaught exception 'FIRInvalidArgumentException', reason: 'Unsupported type: __SwiftValue'
This is my Model Class:
struct StringModel: Identifiable, Codable, Equatable, Hashable {
var id = UUID()
var cross: Int
var mains: Int
var name: String
var date: Date
}
struct UserDataModel: Identifiable, Codable {
#DocumentID public var id: String?
var uid: String
var strings: [StringModel]?
}
Am using the following function to update the value in firestore, however, it doesnt seem to work and throws the error:
#Published var newString = StringModel(cross: 50, date: Timestamp(), mains: 50, name: "")
func addString() {
db.document(uidStr).updateData(["strings" : FieldValue.arrayUnion([newString])]) { err in
if let err = err{
print("\(err)")
}
}
}
Firestore Datatbase
Any ideas how I can go about this? Thanks!
You can't append a Swift object to a Firestore array. You must convert the Swift object into a [String: Any] object. You can add a computed property to the model that outputs this for you.
struct StringModel: Identifiable, Codable, Equatable, Hashable {
var id = UUID()
var cross: Int
var mains: Int
var name: String
var date: Date
var firestoreData: [String: Any] {
return [
"id": id,
"cross": cross,
"mains": mains,
"name": name,
"date": date
]
}
}
func addString() {
db.document(uidStr).updateData(["strings" : FieldValue.arrayUnion([newString.firestoreData])]) { err in
if let err = err{
print("\(err)")
}
}
}

Creating an array from stuct data SwiftUI

First of all, i am very sorry for the noob question, but i just cant seem to figure this out.
I am very new to coding and just started to get my feet wet with SwiftUI, following a few courses and started to dabble in trying to create some basic apps.
I am currently working on an app that does an API call and displays the data.
My issue is, im trying to put the decoded data into an array, it sounds so simple and I think i am missing something very easy, but for the life of me I cant seem to figure it out.
Below is the codable struct I have
struct Drinks: Codable, Identifiable {
let id = UUID()
let strDrink : String
let strInstructions: String
let strDrinkThumb: String?
let strIngredient1: String?
let strIngredient2: String?
let strIngredient3: String?
let strIngredient4: String?
let strIngredient5: String?
}
I want to put the ingredients into an Array so I can go through them in lists etc
import SwiftUI
struct IngredientView: View {
let drink : Drinks
let ingredientArray : [String] = [] // I want to append the ingredients here
var body: some View {
GroupBox() {
DisclosureGroup("Drink Ingredience") {
ForEach(0..<3) { item in
Divider().padding(.vertical, 2)
HStack {
Group {
// To use the array here
}
.font(Font.system(.body).bold())
Spacer(minLength: 25)
}
}
}
}
}
}
Again, sorry for the noob question that probably has a simple answer, but worth a shot asking :D
Thanks!
you could use this approach to get all your ingredients into an array and use
it in Lists. The idea is to use a function to gather all your ingredients into an array of
Ingredient objects. You could also use a computed property.
It is best to use a Ingredient object and declare it Identifiable
so that when you use them in list and ForEach, each one will be
unique, even if the names are the same.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var drinkList = [Drink]()
var body: some View {
List {
ForEach(drinkList) { drink in
VStack {
Text(drink.strDrink).foregroundColor(.blue)
Text(drink.strInstructions)
ForEach(drink.allIngredients()) { ingr in
HStack {
Text(ingr.name).foregroundColor(.red)
Text(ingr.amount).foregroundColor(.black)
}
}
}
}
}
.task {
let theResponse: ApiResponse? = await getData(from: "https://www.thecocktaildb.com/api/json/v1/1/search.php?s=margarita")
if let response = theResponse {
drinkList = response.drinks
}
}
}
func getData<T: Decodable>(from urlString: String) async -> T? {
guard let url = URL(string: urlString) else {
print(URLError(.badURL))
return nil // <-- todo, deal with errors
}
do {
let (data, response) = try await URLSession.shared.data(for: URLRequest(url: url))
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
print(URLError(.badServerResponse))
return nil // <-- todo, deal with errors
}
return try JSONDecoder().decode(T.self, from: data)
}
catch {
print("---> error: \(error)")
return nil // <-- todo, deal with errors
}
}
}
struct ApiResponse: Decodable {
var drinks: [Drink]
}
struct Drink: Decodable, Identifiable {
let id = UUID()
let idDrink: String
let strDrink: String
let strDrinkThumb: String
let strAlcoholic: String
let strGlass: String
let strInstructions: String
let strIngredient1: String?
let strIngredient2: String?
let strIngredient3: String?
let strIngredient4: String?
let strIngredient5: String?
let strIngredient6: String?
let strIngredient7: String?
let strIngredient8: String?
let strIngredient9: String?
let strIngredient10: String?
var strMeasure1: String?
var strMeasure2: String?
var strMeasure3: String?
var strMeasure4: String?
var strMeasure5: String?
var strMeasure6: String?
var strMeasure7: String?
var strMeasure8: String?
var strMeasure9: String?
var strMeasure10: String?
// --- here adjust to your needs, could also use a computed property
func allIngredients() -> [Ingredient] {
return [
Ingredient(name: strIngredient1 ?? "", amount: strMeasure1 ?? ""),
Ingredient(name: strIngredient2 ?? "", amount: strMeasure2 ?? ""),
Ingredient(name: strIngredient3 ?? "", amount: strMeasure3 ?? ""),
Ingredient(name: strIngredient4 ?? "", amount: strMeasure4 ?? ""),
Ingredient(name: strIngredient5 ?? "", amount: strMeasure5 ?? ""),
Ingredient(name: strIngredient6 ?? "", amount: strMeasure6 ?? ""),
Ingredient(name: strIngredient7 ?? "", amount: strMeasure7 ?? ""),
Ingredient(name: strIngredient8 ?? "", amount: strMeasure8 ?? ""),
Ingredient(name: strIngredient9 ?? "", amount: strMeasure9 ?? ""),
Ingredient(name: strIngredient10 ?? "", amount: strMeasure10 ?? "")
].filter{!$0.name.isEmpty}
}
}
struct Ingredient: Identifiable {
let id = UUID()
var name: String
var amount: String
}
change you struct to
struct Drink: Codable, Identifiable {
let id = UUID()
let strDrink : String
let strInstructions: String
let strDrinkThumb: String?
let strIngredients: [String] = []
}
wherever you want to loop through ingredients you can use drink.strIngredients array
First of all your design cannot work because you are ignoring the root object, a struct with a key drinks
struct Root : Decodable {
let drinks : [Drink]
}
A possible solution is to write a custom init method. However this a bit tricky because you have to inject dynamic CodingKeys to be able to decode the ingredients in a loop
First create a custom CodingKey struct
struct AnyKey: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) { return nil } // will never be called
}
In the Drink struct – by the way it's supposed to be named in singular form – specify a second container, create the ingredient keys on the fly, decode the ingredients in a loop and append the results to an array until the value is nil. Somebody should tell the owners of the service that their JSON structure is very amateurish. As you have to specify the CodingKeys anyway I mapped the keys to more meaningful and less redundant struct member names.
struct Drink: Decodable, Identifiable {
private enum CodingKeys : String, CodingKey {
case name = "strDrink", instructions = "strInstructions", thumbnail = "strDrinkThumb"
}
let id = UUID()
let name: String
let instructions: String
let thumbnail: URL
let ingredients: [String]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.instructions = try container.decode(String.self, forKey: .instructions)
self.thumbnail = try container.decode(URL.self, forKey: .thumbnail)
var counter = 1
var temp = [String]()
let anyContainer = try decoder.container(keyedBy: AnyKey.self)
while true {
let ingredientKey = AnyKey(stringValue: "strIngredient\(counter)")!
guard let ingredient = try anyContainer.decodeIfPresent(String.self, forKey: ingredientKey) else { break }
temp.append(ingredient)
counter += 1
}
ingredients = temp
}
}
In the view display the ingredients like this
struct IngredientView: View {
let drink : Drink
var body: some View {
GroupBox() {
DisclosureGroup("Drink Ingredience") {
ForEach(drink.ingredients) { ingredient in
Divider().padding(.vertical, 2)
HStack {
Group {
Text(ingredient)
}
.font(Font.system(.body).bold())
Spacer(minLength: 25)
}
}
}
}
}
}

How to parse this JSON format in swift

I have this JSON format:
{
"version":"7.0.19",
"fields": ["ID","pm","age","pm_0","pm_1","pm_2","pm_3","pm_4","pm_5","pm_6","conf","pm1","pm_10","p1","p2","p3","p4","p5","p6","Humidity","Temperature","Pressure","Elevation","Type","Label","Lat","Lon","Icon","isOwner","Flags","Voc","Ozone1","Adc","CH"],
"data":[[20,0.0,1,0.0,0.0,0.0,0.0,0.0,0.0,0.0,97,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,null,null,null,1413,0,"Oakdale",40.603077,-111.83612,0,0,0,null,null,0.01,1]],
"count":11880
}
but I cannot work out how to use a Codable protocol to parse the json response.
this would be my desired model.
struct Point: Codable {
let pm2: String?
let latitude, longitude: Double?
let temp: String?
let iD: String?
enum CodingKeys: String, CodingKey {
case pm2 = "pm", temp = "Temperature", iD = "ID", latitude = "Lat", longitude = "Lon"
}
}
Here is a URL to the json
https://webbfoot.com/dataa.json
You can use Codable to parse this:
struct Response: Decodable {
let version: String
let fields: [String]
let data: [[QuantumValue?]]
let count: Int
}
enter code here
enum QuantumValue: Decodable {
case float(Float), string(String)
init(from decoder: Decoder) throws {
if let int = try? decoder.singleValueContainer().decode(Float.self) {
self = .float(float)
return
}
if let string = try? decoder.singleValueContainer().decode(String.self) {
self = .string(string)
return
}
throw QuantumError.missingValue
}
enum QuantumError:Error {
case missingValue
}
}
QuantumValue will handle both Float and String and ? will handle the null part.
This one is tricky and requires manual decoding. The principle would be to define a mapping between the fields you expect to decode and the properties of your object, then depending on the type of the property (String, Double, etc...), attempt to decode the data.
Second, since you have an array of points, you need some kind of container object to hold the array, for example:
struct Points {
var data: [Point] = []
}
First, some of your model properties don't match the type in the data, e.g. iD is a String, but the data has an Int. For simplicity, I'll redefine your model to match the data
struct Point {
var pm2: Int? = nil
var latitude: Double? = nil
var longitude: Double? = nil
var temp: Int? = nil
var iD: Int? = nil
}
Now, write the manual decoder for the parent container Points:
extension Points: Decodable {
static let mapping: [String: PartialKeyPath<Point>] = [
"pm": \Point.pm2,
"Lat": \Point.latitude,
"Lon": \Point.longitude,
"Temperature": \Point.temp,
"ID": \Point.iD
]
enum CodingKeys: CodingKey { case fields, data }
private struct Dummy: Decodable {} // see below why
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let fields = try container.decode([String].self, forKey: .fields)
var data = try container.nestedUnkeyedContainer(forKey: .data)
while !data.isAtEnd {
var row = try data.nestedUnkeyedContainer()
var point = Point()
for field in fields {
let keyPath = Points.mapping[field]
switch keyPath {
case let kp as WritableKeyPath<Point, String?>:
point[keyPath: kp] = try row.decodeIfPresent(String.self)
case let kp as WritableKeyPath<Point, Int?>:
point[keyPath: kp] = try row.decodeIfPresent(Int.self)
case let kp as WritableKeyPath<Point, Double?>:
point[keyPath: kp] = try row.decodeIfPresent(Double.self)
default:
// this is a hack to skip this value
let _ = try? row.decode(Dummy.self)
}
}
self.data.append(point)
}
}
}
Once you have that, you can decode the JSON like so:
let points = try JSONDecoder().decode(Points.self, from: jsonData)
let firstPoint = points.data[0]

Accessing embedded JSON using decodable in Swift 4

I am trying to access a particular an embedded array of dictionaries to create my swift objects. I am unsure about how to access that array in the JSON dictionary.
Here is the definition of my Swift object = StarWarsPeople
class StarWarsPeople: Decodable {
var name: String?
var height: String?
var weight: String?
var hair_color: String?
var skin_color: String?
var eye_color: String?
var birth_year: String?
var gender: String?
}
Here is my APIClient class:
class StarWarsPeopleAPIClient
{
class func getStarWarsPeopleInformation (page: Int, completion:#escaping ([StarWarsPeople])-> ()) throws {
let starWarsPeopleURL = "https://swapi.co/api/people/?page=\(page)"
let convertedStarWarsPeopleURL = URL(string: starWarsPeopleURL)
guard let unwrappedConvertedStarWarsPeopleURL = convertedStarWarsPeopleURL else { print("unwrappedConvertedStarWarsPeopleURL did not unwrap"); return}
let request = URLRequest(url: unwrappedConvertedStarWarsPeopleURL)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let unwrappedData = data else { print("unwrappedData did not unwrap"); return}
do {
let starWarsPeopleDataArray = try JSONDecoder().decode([StarWarsPeople].self, from: unwrappedData)
completion(starWarsPeopleDataArray)
}
catch let error {
print("Error occured here: \(error.localizedDescription)")
}
}
task.resume()
}
}
Here is my Json, it is the results array that I would like to access, it is an array of dictionaries, which I need to iterate over to create my StarWarsPeople Object.
{
"count": 87,
"next": "url",
"previous": null,
"results": [
{
"name": "Luke Skywalker",
"height": "172",
"mass": "77",
"hair_color": "blond",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": "19BBY",
"gender": "male",
"homeworld": "url",
"films": [
"url",
"url",
"url",
"url",
"url"
],
"species": [
"url"
],
"vehicles": [
"url",
"url"
Please read the JSON. You are ignoring the enclosing object
struct Root: Decodable {
let count: Int
let next: URL?
let previous: URL?
let results : [StarWarsPeople]
}
struct StarWarsPeople: Decodable {
private enum CodingKeys: String, CodingKey {
case name, height, mass
case hairColor = "hair_color", skinColor = "skin_color"
case eyeColor = "eye_color", birthYear = "birth_year", gender
}
let name: String
let height: String
let mass: String
let hairColor: String
let skinColor: String
let eyeColor: String
let birthYear: String
let gender: String
}
...
let root = try JSONDecoder().decode(Root.self, from: unwrappedData)
let starWarsPeopleDataArray = root.results
...
Notes:
A struct is sufficient.
Map the snake_cased keys to camelCased properties.
In almost all cases the properties can be declared as constants (let).
Don't declare all properties schematically as optional. Declare only those as optional which corresponding key can be missing or the value can be null.
Simply define a wrapper struct that holds the results property from the JSON response.
struct ApiResponse: Decodable {
results: [StarWarsPeople]
}
and later use
let apiResponse = try JSONDecoder().decode(ApiResponse.self, from: unwrappedData)
let starWarsPeopleDataArray = apiResponse.results
to parse it.
Results you are trying to fetch is actually present in results keys. Also we need to use properties same as parameter name( we can use CodingKeys enum too for overriding this).
So, first parse outer JSON , In new struct say StarWarsPeopleParent
class StarWarsPeopleParent: Decodable {
var count: Int?
var results: [StarWarsPeople]?
}
Update your StarWarsPeople struct's properties as:
class StarWarsPeople: Decodable {
var name: String?
var height: String?
var mass: String?
var hair_color: String?
var skin_color: String?
var eye_color: String?
var birth_year: String?
var gender: String?
}
Then parse it like:
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let unwrappedData = data else { print("unwrappedData did not unwrap"); return}
do {
let starWarsPeopleDataArray = try JSONDecoder().decode(StarWarsPeopleParent.self, from: unwrappedData)
completion(starWarsPeopleDataArray)
}
catch let error {
print("Error occured here: \(error.localizedDescription)")
}
}
Hope this is fine for you to understand.

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