This bounty has ended. Answers to this question are eligible for a +50 reputation bounty. Bounty grace period ends in 13 hours.
Dipesh Pokhrel wants to draw more attention to this question:
I have gone throught the documentation no where its specifically mentioned. how to deal with the multidimensional string objects
I have a realm class which contains the a multi dimensional string, realm in Decodable is throwing an error to while parsing, created class to support realm.
class Categories : Object,Decodable {
// var assetSum : [[String]]? // In swift originally
let assetSum = RealmSwift.List<String>() // modified to support list
#objc var id : String?
#objc var dn : String?
How to fix this , to be more Generalise how to store var assetSum : [[String]]? this kind of value in realm?
I have gone through the documentation of realm but could not find something related to this
Realm supports basic types like Int, String, Date etc. and several collections types like List (Array), Map (Dictionary) from the box. For the other your custom types you can use json serialization which works pretty quick.
It can be implemented with two variables where persistent private one is for storing data and public one is for accessing e.g:
import RealmSwift
class Categories: Object {
#Persisted private var assetSum: Data?
var assetSumValue: [[String]]? {
get {
guard let value = assetSum else {
return nil
}
return try? JSONDecoder().decode(([[String]]?).self, from: value)
}
set {
assetSum = try? JSONEncoder().encode(newValue)
}
}
}
Now you can easy set/get values with assetSumValue:
// Create and save
let categories = Categories()
try realm.write {
categories.assetSumValue = [["1", "2", "3"], ["4", "5", "6"]]
realm.add(categories)
}
// Get first element from DB
if let categories = realm.objects(Categories.self).first,
let value = categories.assetSumValue
{
print(value) // Prints: [["1", "2", "3"], ["4", "5", "6"]]
}
In case of encoding/decoding your custom Realm types with complex properties you should implement a custom decoder:
class Categories: Object, Codable {
#Persisted var id: String?
#Persisted var dn: String?
#Persisted private var assetSum: Data?
var assetSumValue: [[String]]? {
get {
guard let value = assetSum else {
return nil
}
return try? JSONDecoder().decode(([[String]]?).self, from: value)
}
set {
assetSum = try? JSONEncoder().encode(newValue)
}
}
override init() {
super.init()
}
required init(from decoder: Decoder) throws {
super.init()
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode((String?).self, forKey: .id)
dn = try values.decode((String?).self, forKey: .dn)
assetSumValue = try values.decode(([[String]]?).self, forKey: .assetSum)
}
}
How to decode:
let json = """
{
"id": "100",
"dn": "200",
"assetSum": [
["one", "two", "three"],
["four", "five", "six"]
]
}
"""
let categories = try JSONDecoder().decode(Categories.self, from: json.data(using: .utf8)!)
if let value = categories.assetSumValue {
print(value) // Prints [["one", "two", "three"], ["four", "five", "six"]]
}
Related
This question already has answers here:
Decoding two different JSON responses with one struct using Codable
(2 answers)
Closed 1 year ago.
I'm receiving the same json structure from two endpoints, the only thing different are the keys in the json. On response #1 I get
[
{
"id": 45,
"chapter__book__name": "Alonso",
"chapter__book__id": 70,
"chapter__chapter": 2,
"verse": "",
"verse_number": 5,
"chapter": 97
},
]
And on response #2 I get:
[
{
"id": 962,
"book_name": "Title here",
"book_id": 70,
"chapter_number": 32,
"verse": "xxx",
"verse_number": 24,
"chapter": 127
},
]
Can one struct decode both of these? Currently my struct looks like this:
struct Verse: Decodable, Identifiable {
let id: Int
let book_name: String
let book_id: Int
let verse: String
let verse_number: Int
let chapter: Int // chapter Id in database
let chapter_number: Int
}
Which matches response #2, but not response #1.
#lorem ipsum's method should work I didn't try it myself with swiftUI, however it feels a bit convoluted to deal with 2 different types of object. Eventhough they share a common protocol, since it's the same object that will be decoded, it seems natural to keep track of one single type.
As stated by #Larme it can be done with a custom init(from decoder: Decoder) method.
import UIKit
let jsonA = """
[
{
"id": 45,
"chapter__book__name": "Alonso",
"chapter__book__id": 70,
"chapter__chapter": 2,
"verse": "",
"verse_number": 5,
"chapter": 97
},
]
"""
let jsonB = """
[
{
"id": 962,
"book_name": "Title here",
"book_id": 70,
"chapter_number": 32,
"verse": "xxx",
"verse_number": 24,
"chapter": 127
},
]
"""
protocol VerseCodingKey: CodingKey {
static var id: Self { get }
static var book_name: Self { get }
static var book_id: Self { get }
static var verse: Self { get }
static var verse_number: Self { get }
static var chapter: Self { get }
static var chapter_number: Self { get }
}
struct Verse: Decodable {
var id: Int
var book_name: String
var book_id: Int
var verse: String
var verse_number: Int
var chapter: Int
var chapter_number: Int
enum CodingKeysA: String, VerseCodingKey {
case id
case book_name
case book_id
case verse
case verse_number
case chapter
case chapter_number
}
enum CodingKeysB: String, VerseCodingKey {
case id
case book_name = "chapter__book__name"
case book_id = "chapter__book__id"
case verse
case verse_number
case chapter = "chapter__chapter"
case chapter_number = "chapter"
}
init(from decoder: Decoder) throws {
do {
try self.init(from: decoder, verseCodingKey: CodingKeysA.self)
return
} catch { }
do {
try self.init(from: decoder, verseCodingKey: CodingKeysB.self)
return
} catch { }
throw CustomError.unmatchedCodingKeys
}
init<T: VerseCodingKey>(from decoder: Decoder, verseCodingKey: T.Type) throws {
do {
let values = try decoder.container(keyedBy: T.self)
id = try values.decode(Int.self, forKey: .id)
book_name = try values.decode(String.self, forKey: .book_name)
book_id = try values.decode(Int.self, forKey: .book_id)
verse = try values.decode(String.self, forKey: .verse)
verse_number = try values.decode(Int.self, forKey: .verse_number)
chapter = try values.decode(Int.self, forKey: .chapter)
chapter_number = try values.decode(Int.self, forKey: .chapter_number)
} catch {
throw CustomError.missingCodingKey
}
}
}
enum CustomError: Error {
case missingCodingKey
case unmatchedCodingKeys
}
let dataA = jsonA.data(using: .utf8)!
let dataB = jsonB.data(using: .utf8)!
let verseA = try? JSONDecoder().decode([Verse].self, from: dataA)
let verseB = try? JSONDecoder().decode([Verse].self, from: dataB)
This code works on playground
SideNotes:
The whole point is to juggle with two different CodingKeys.
since this evolution it is now feasible to make an enum conform to protocols, which I didn't now of before diving into your issue. This makes the code more straightforward and reusable.
There may be a better way to handle the do catch mechanism but it's acceptable at this point. as stated by #Cristik in comment, you should enhance the error handling mechanism because you don't want to let all the error going through. see his comment below
This is how far I could get with this little experiment, I reckon someone will be able to do better. It still seem more reliable to use a single concrete class instead of two plus a protocol, but again, I'm not pretending to be an expert.
I've read this and this and spent many hours perusing SO, but I can't seem to figure out how to properly use Decode with an empty array.
Original Code
At first, I had no problems with my code.
Here was my original struct:
struct JobHabit: Codable {
var name: String
var description: String
var assigned: String
var order: Int
var category: Category
var active: Bool
var altName: String
var altNameDate: Double
var altAssigned: String
var altCategory: Category
var altOrder: Int
enum Category: String, Codable {
case dailyJobMorning
case dailyJobEvening
case weeklyJob1
case weeklyJob2
case jobBonus
case quickPoints
case jobJar
case dailyHabit
case weeklyHabit
case habitBonus
}
}
And here was my function:
static func observeJobs(completion: #escaping () -> Void) {
FB.ref
.child(FB.jobs)
.observe(.value, with: { (snapshot) in
guard snapshot.exists() else {
completion()
return
}
for item in snapshot.children {
guard let snap = item as? DataSnapshot else { return }
guard let value = snap.value as? [String: Any] else { return }
do {
let jsonData = try JSONSerialization.data(withJSONObject: value, options: [])
let habit = try JSONDecoder().decode(JobHabit.self, from: jsonData)
jobsMasterList.append(habit)
} catch let error {
print(error)
manuallyDecodeJobHabitAndAddToArray(.dailyJobMorning, value)
}
}
completion()
})
}
That all worked great. No problems.
But then...
I added in another parameter to the struct. I added in a points parameter that could potentially be empty. It's not optional, but it could be empty. (It's the last parameter.)
Like this:
struct JobHabit: Codable {
var name: String
var description: String
var assigned: String
var order: Int
var category: Category
var active: Bool
var altName: String
var altNameDate: Double
var altAssigned: String
var altCategory: Category
var altOrder: Int
var points: [Point] // <=== new parameter that messed everything up
}
And that caused the decode function to fail with this error:
keyNotFound(CodingKeys(stringValue: "points", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "points", intValue: nil) ("points").", underlyingError: nil))
The error makes sense. The decoder can't find a value. Well, if it's empty, then Firebase will return nothing. So that's expected behavior. But why can't the decoder account for that?
I read up on decoding optional values and came up with an initializer for the struct, like so:
struct JobHabit: Codable {
var name: String
var description: String
var assigned: String
var order: Int
var category: Category
var active: Bool
var altName: String
var altNameDate: Double
var altAssigned: String
var altCategory: Category
var altOrder: Int
var points: [Point]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.description = try container.decode(String.self, forKey: .description)
self.assigned = try container.decode(String.self, forKey: .assigned)
self.order = try container.decode(Int.self, forKey: .order)
self.category = try container.decode(Category.self, forKey: .category)
self.active = try container.decode(Bool.self, forKey: .active)
self.altName = try container.decode(String.self, forKey: .altName)
self.altNameDate = try container.decode(Double.self, forKey: .altNameDate)
self.altAssigned = try container.decode(String.self, forKey: .altAssigned)
self.altCategory = try container.decode(Category.self, forKey: .altCategory)
self.altOrder = try container.decode(Int.self, forKey: .altOrder)
self.points = try container.decodeIfPresent([Point].self, forKey: .points) ?? []
}
}
That created a new issue. The issue is that I can't create new instances of my struct anywhere else in the app. When I try doing this:
static let job120 = JobHabit(name: "π³ Bills & finances",
description: "record receipts\nupdate accounts\npay bills\nfiling (max 1 hour)",
assigned: "none",
order: 19,
category: .weeklyJob1,
active: false,
altName: "π³ Bills & finances",
altNameDate: 0,
altAssigned: "none",
altCategory: .weeklyJob1,
altOrder: 19,
points: [])
I get the following error message:
Extra argument 'name' in call
Apparently, the initializer is making it so I can't create new instances of the struct? I'm really struggling to figure out how to decode a potentially empty array from Firebase.
What am I doing wrong? Everything worked fine with decoding BEFORE I added in that empty points array. Once I added in the points parameter, decode couldn't handle it, and so I had to add in the initializers in the struct. Now my other code doesn't work.
Oh, and here is my JSON tree sample:
{
"-MOESVPtiXtXQh19sWBP" : {
"active" : true,
"altAssigned" : "Dad",
"altCategory" : "dailyJobMorning",
"altName" : "π Morning Job Inspections",
"altNameDate" : 0,
"altOrder" : 0,
"assigned" : "Dad",
"category" : "dailyJobMorning",
"description" : "set job timer\nvisually inspect each person's job...",
"name" : "π Morning Job Inspections",
"order" : 0
}
}
When you have an API, which can return a struct with either filled or empty value, you must declare this variable as an optional. In other case how are you going to handle it in your code later?
As apple documentation says, an optional is:
A type that represents either a wrapped value or nil, the absence of a value.
Doesn't this make sense?
So to fix existing issues just declare your points variable as [Point]?. If there is no value for points presented in your API response, it will remain nil. Later in your code use standard optionals' unwrappers to check whether there is value presented or not.
I have a variable identity with type ETIdentity that I need to store in ViewController1 and retrieve in ViewController2,
ViewController1
//Variables
var activationCodeFromCore, serialNumberFromCore, entityNameFromCore, deviceIdFromCore, registrationCodeFromCore, entityFromCore: String?
var activationCode, serialNumber, entityName, deviceId, registrationCode, entity: String?
var counter: Int = 0
var storedIdentity: ETIdentity?
Below is the storedIdentity that I need to keep
let storedIdentity = BridgeSDKUtils.performClassicActivation("26586-05858", withActivationCode: "8998-6857-1357-1870", "entidad0");
GlobalIdentity.identity = storedIdentity;
func softTokenDataService() {
let storedIdentity = BridgeSDKUtils.performClassicActivation("26586-05858", withActivationCode: "8998-6857-1357-1870", "entidad0");
GlobalIdentity.identity = storedIdentity;
self.activationCode = "8998-6857-1357-1870"
self.serialNumber = "26586-05858"
self.entityName = "entityData\(counter)"
self.deviceId = "\(String(describing: storedIdentity?.deviceId))"
self.registrationCode = "\(String(describing: storedIdentity?.registrationCode))"
self.entity = "\(storedIdentity!)"
}
...
func getEntityCore()
{
//Variables that are going to be stored
self.activationCodeFromCore = activationCode
self.serialNumberFromCore = serialNumber
self.entityNameFromCore = entityName
self.deviceIdFromCore = deviceId
self.registrationCodeFromCore = registrationCode
self.entityFromCore = storedIdentity
}
...
//SecureStorage Function
func saveEntityToCoreData()-> Bool {
self.softTokenDataService()
var SavedItem:Bool = true
var arr : [[String: Any]] = [[
"activationCode": self.activationCodeFromCore,
"serialNumber": self.serialNumberFromCore,
"entityName": self.entityNameFromCore,
"deviceId": self.deviceIdFromCore,
"registrationCode": self.registrationCodeFromCore,
"entity": self.entityFromCore]]
let jsonData = try! JSONSerialization.data(withJSONObject: arr, options: [.prettyPrinted])
let json = String(data: jsonData, encoding: String.Encoding.utf8)!
if self.saveRutSwitchOn
{
SecureData.save(key: "entityData0)", data: json.data(using: .utf8)!)
}
SavedItem = self.saveRutSwitchOn
return SavedItem
}
ViewController2
struct Person {
var activationCode: String
var serialNumber: String
var entityName: String
var deviceId: String
var registrationCode: String
var entity: String
}
struct EntityModel: Codable {
let activationCode, serialNumber, entityName, deviceId, registrationCode, entity: String?
}
if let loadedData = SecureData.load(key: "entityData0") {
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let entityData = try decoder.decode([EntityModel].self, from: loadedData)
entityData.forEach { (EntityModel) in
//Here I Imagine something like this
//var identity: ETIdentity?
//identity = EntityModel.entity
////Here I have the identity, so I can manipulate it like needed, because is from type ETIdentity I can access its methods.
//identity?.getOTP(Date())
}
} catch {
print(error)
}
}
Reference Data:
GlobalIdentity.swift
struct GlobalIdentity{
static var identity : ETIdentity?
}
ETIdentity.h
#interface ETIdentity : NSObject<NSCoding> {
#private
-(NSString*)getOTP:(NSDate*)time;
#end
EDIT
The problem is that the variable entity (where I need to call its parameters in ViewController2), is not a String, so it crashes, it doesn't work. I also tried to put the variable identity with the type I needed var identity: ETIdentity?, but ETIdentity isn't in protocol with Codable (to work with struct so I can call them in ViewController2)
I read your question and found basic thing that you are missing is your entity seems to be another model like a dictionary or some other type if it is dictionary then do this. Currently your SecureData.load(key: "entityData0") contain all the data that return array of entity models, And your Data object loadedData must contain entity object, Why not you try to implement Codable for your entity like this.
struct SortedArryModel: Codable {
var sorterarrkey: String? // if your array contain strings
}
And use this SortedArryModel in your EntityModel
after that you can get your array like [key:string], just convert that string to array.
Suggestion: in your case better to use JsonSerialization instead of codable, if you still want to apply codable then your elements should conform codable protocol. Like I mentioned above, implement your entity as model that confrom codable protocol.
I want to save the array patientList in UserDefaults. Patient is an custom class so I need to transfer it into Data object, but this doesn't work on Swift 5 like it did before.
func addFirstPatient(){
let newPatient = Patient(name: nameField.text!, number: numberField.text!, resultArray: resultArray, diagnoseArray: diagnoseArray)
let patientList: [Patient] = [newPatient]
let encodeData: Data = NSKeyedArchiver.archivedData(withRootObject: patientList)
UserDefaults.standard.set(encodeData, forKey: "patientList")
UserDefaults.standard.synchronize()
}
struct Patient {
var diagnoseArray: [Diagnose]
var resultArray: [Diagnose]
var name: String
var number: String
init(name: String, number: String, resultArray: [Diagnose], diagnoseArray: [Diagnose]) {
self.diagnoseArray = diagnoseArray
self.name = name
self.number = number
self.resultArray = resultArray
}
}
struct Diagnose{
var name: String
var treatments: [Treatment]
var isPositiv = false
var isExtended = false
init(name: String, treatments: [Treatment]) {
self.name = name
self.treatments = treatments
}
}
struct Treatment {
var name: String
var wasMade = false
init(name: String) {
self.name = name
}
}
This is what the function looks like.
The problem is in the line where I initialize encodeData.
let encodeData: Data = try! NSKeyedArchiver.archivedData(withRootObject: patientList, requiringSecureCoding: false)
This is what Swift suggests but when I try it like this it always crashes and I don't get the error
You cannot use NSKeyedArchiver with structs at all. The objects must be subclasses of NSObject which adopt NSCoding and implement the required methods.
As suggested in the comments Codable is the better choice for example
struct Patient : Codable {
var name: String
var number: String
var resultArray: [Diagnose]
var diagnoseArray: [Diagnose]
}
struct Diagnose : Codable {
var name: String
var treatments: [Treatment]
var isPositiv : Bool
var isExtended : Bool
}
struct Treatment : Codable {
var name: String
var wasMade : Bool
}
let newPatient = Patient(name: "John Doe",
number: "123",
resultArray: [Diagnose(name: "Result", treatments: [Treatment(name: "Treat1", wasMade: false)], isPositiv: false, isExtended: false)],
diagnoseArray: [Diagnose(name: "Diagnose", treatments: [Treatment(name: "Treat2", wasMade: false)], isPositiv: false, isExtended: false)])
let patientList: [Patient] = [newPatient]
do {
let encodeData = try JSONEncoder().encode(patientList)
UserDefaults.standard.set(encodeData, forKey: "patientList")
// synchronize is not needed
} catch { print(error) }
If you want to provide default values for the Bool values you have to write an initializer.
Vadian's answer is correct, you cannot use NSKeyedArchiver with structs. Having all your objects conform to Codable is the best way to reproduce the behavior you are looking for. I do what Vadian does, but I you can also use protocol extensions to make this safer.
import UIKit
struct Patient: Codable {
var name: String
var number: String
var resultArray: [Diagnose]
var diagnoseArray: [Diagnose]
}
struct Diagnose: Codable {
var name: String
var treatments: [Treatment]
var isPositiv : Bool
var isExtended : Bool
}
struct Treatment: Codable {
var name: String
var wasMade : Bool
}
let newPatient = Patient(name: "John Doe",
number: "123",
resultArray: [Diagnose(name: "Result", treatments: [Treatment(name: "Treat1", wasMade: false)], isPositiv: false, isExtended: false)],
diagnoseArray: [Diagnose(name: "Diagnose", treatments: [Treatment(name: "Treat2", wasMade: false)], isPositiv: false, isExtended: false)])
let patientList: [Patient] = [newPatient]
Introduce a protocol to manage the encoding and saving of objects.
This does not have to inherit from Codable but it does for this example for simplicity.
/// Objects conforming to `CanSaveToDisk` have a save method and provide keys for saving individual objects or a list of objects.
protocol CanSaveToDisk: Codable {
/// Provide default logic for encoding this value.
static var defaultEncoder: JSONEncoder { get }
/// This key is used to save the individual object to disk. This works best by using a unique identifier.
var storageKeyForObject: String { get }
/// This key is used to save a list of these objects to disk. Any array of items conforming to `CanSaveToDisk` has the option to save as well.
static var storageKeyForListofObjects: String { get }
/// Persists the object to disk.
///
/// - Throws: useful to throw an error from an encoder or a custom error if you use stage different from user defaults like the keychain
func save() throws
}
Using protocol extensions we add an option to save an array of these objects.
extension Array where Element: CanSaveToDisk {
func dataValue() throws -> Data {
return try Element.defaultEncoder.encode(self)
}
func save() throws {
let storage = UserDefaults.standard
storage.set(try dataValue(), forKey: Element.storageKeyForListofObjects)
}
}
We extend our patient object so it can know what to do when saving.
I use "storage" so that this could be swapped with NSKeychain. If you are saving sensitive data (like patient information) you should be using the keychain instead of UserDefaults. Also, make sure you comply with security and privacy best practices for health data in whatever market you're offering your app. Laws can be a very different experience between countries. UserDefaults might not be safe enough storage.
There are lots of great keychain wrappers to make things easier. UserDefaults simply sets data using a key. The Keychain does the same. A wrapper like https://github.com/evgenyneu/keychain-swift will behave similar to how I use UserDefaults below. I have commented out what the equivalent use would look like for completeness.
extension Patient: CanSaveToDisk {
static var defaultEncoder: JSONEncoder {
let encoder = JSONEncoder()
// add additional customization here
// like dates or data handling
return encoder
}
var storageKeyForObject: String {
// "com.myapp.patient.123"
return "com.myapp.patient.\(number)"
}
static var storageKeyForListofObjects: String {
return "com.myapp.patientList"
}
func save() throws {
// you could also save to the keychain easily
//let keychain = KeychainSwift()
//keychain.set(dataObject, forKey: storageKeyForObject)
let data = try Patient.defaultEncoder.encode(self)
let storage = UserDefaults.standard
storage.setValue(data, forKey: storageKeyForObject)
}
}
Saving is simplified, check out the 2 examples below!
do {
// saving just one patient record
// this saves this patient to the storageKeyForObject
try patientList.first?.save()
// saving the entire list
try patientList.save()
} catch { print(error) }
struct Employee: Codable{
var name: String
}
var emp1 = Employee(name: "John")
let encoder = JSONEncoder()
do {
let data = try encoder.encode(emp1)
UserDefaults.standard.set(data, forKey: "employee")
UserDefaults.standard.synchronize()
} catch {
print("error")
}
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")]