简体   繁体   中英

JSON parsing in Swift. When value is not nil, I can parse the JSON data. If value is nil, I receive an empty array instead of nil

So, let me detail a bit with an example:

Lets say I want to get data regarding Persons. Every person has 3 collections: Friends, Family and Work.

Friends collection contains: Person objects(which describes every friend: name age aso)

Family collection contains: Person objects(which describes every family member: name age aso)

Work collection contains: Person objects(which describes every work partner: name age aso)

So it will be something like this:

Me {

age:25,

name: Dan,

Friends [{age:21,name:Andrew}, {age:30,name:Mars}]

Family [{age:21,name:Andrew}, {age:30,name:Mars}]

Work [{age:21,name:Andrew}, {age:30,name:Mars}]

}

Now, having this example in mind. If I don't want to specify my name, the instead of "age:25" I will have "age:nil", right? Because age is an Int?, an obtional.

My problem is, when I parse the JSON data, instead of receiving a nil value (from that obtional age:Int?), I receive an empty array of type [Int], so inteased of nil, I receive [].

Now, this is my example:

class FinancialData: Codable {
    
    var maxAge:Int?
    var currentPrice:CurrentPrice?
    var targetHighPrice:TargetHighPrice?
    var targetLowPrice:TargetLowPrice?
    var targetMeanPrice:TargetMeanPrice?
    var targetMedianPrice:TargetMedianPrice?
    var recommendationMean:RecommendationMean?
    var recommendationKey, financialCurrency:String?
    var numberOfAnalystOpinions:NumberOfAnalystOpinions?
    var totalCash:TotalCash?
    var totalCashPerShare:TotalCashPerShare?
    var ebitda:Ebitda
    var totalDebt:TotalDebt?
    var quickRatio:QuickRatio?
    var currentRatio:CurrentRatio?
    var totalRevenue:TotalRevenue?
    var debtToEquity:DebtToEquity?
    var revenuePerShare:RevenuePerShare?
    var returnOnAssets:ReturnOnAssets?
    var returnOnEquity:ReturnOnEquity?
    var grossProfits:GrossProfits?
    var freeCashflow:FreeCashFlow?
    var operatingCashflow:OperatingCashFlow?
//    var earningsGrowth:EarningsGrowth?
    var revenueGrowth:RevenueGrowth?
    var grossMargins:GrossMargins?
    var ebitdaMargins:EbitdaMargins?
    var operatingMargins:OperatingMargins?
    var profitMargins:ProfitMargins?
    
    enum CodingKeys: String, CodingKey {
        
        case maxAge = "maxAge"
        case currentPrice = "currentPrice"
        case targetHighPrice = "targetHighPrice"
        case targetLowPrice = "targetLowPrice"
        case targetMeanPrice = "targetMeanPrice"
        case targetMedianPrice = "targetMedianPrice"
        case recommendationMean = "recommendationMean"
        case recommendationKey = "recommendationKey"
        case numberOfAnalystOpinions = "numberOfAnalystOpinions"
        case totalCash = "totalCash"
        case totalCashPerShare = "totalCashPerShare"
        case ebitda = "ebitda"
        case totalDebt = "totalDebt"
        case quickRatio = "quickRatio"
        case currentRatio = "currentRatio"
        case totalRevenue = "totalRevenue"
        case debtToEquity = "debtToEquity"
        case revenuePerShare = "revenuePerShare"
        case returnOnAssets = "returnOnAssets"
        case returnOnEquity = "returnOnEquity"
        case grossProfits = "grossProfits"
        case freeCashflow = "freeCashflow"
        case operatingCashflow = "operatingCashflow"
     //   case earningsGrowth = "earningsGrowth"
        case revenueGrowth = "revenueGrowth"
        case grossMargins = "grossMargins"
        case ebitdaMargins = "ebitdaMargins"
        case operatingMargins = "operatingMargins"
        case profitMargins = "profitMargins"
        case financialCurrency = "financialCurrency"
    }
}

//....//

class Ebitda: Codable {
    
    var raw:Double?
    var fmt, longFmt: String?
    
    enum CodingKeys: String, CodingKey {
        
        case raw = "raw"
        case fmt = "fmt"
        case longFmt = "longFmt"
    }
}

Ebitda is the problem, is a class. If there is no Ebitda class at my API, I am not receiving an Obtional nil value of Ebitda?, I am receiving an empty array of Ebitda, and my program crashes. I can't modify Ebitda? to [Ebitda] because when the value is NOT nil, I receive an Ebitda object, not an array of Ebitda objects.

So, this is the error when Ebitda is nil, but I receive an empty array instead of nil:

typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "financialData", intValue: nil), CodingKeys(stringValue: "ebitda", intValue: nil)], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil)).

I will attach a short description of the JSON data I import, Look for ebitda:

{
    
    
    "financialData": {
        "maxAge": 86400,
        "recommendationKey": "buy",
        "numberOfAnalystOpinions": {
            "raw": 23,
            "fmt": "23",
            "longFmt": "23"
        },
        "totalCash": {
            "raw": 848978968576,
            "fmt": "848.98B",
            "longFmt": "848,978,968,576"
        },
        "totalCashPerShare": {
            "raw": 106.165,
            "fmt": "106.17"
        },
        "ebitda": [],
        "totalDebt": {
            "raw": 543510003712,
            "fmt": "543.51B",
            "longFmt": "543,510,003,712"
        },
        "quickRatio": [],
        "currentRatio": [],
        "debtToEquity": [],
        "revenuePerShare": {
            "raw": 11.389,
            "fmt": "11.39"
        },
        "returnOnAssets": {
            "raw": 0.00885,
            "fmt": "0.88%"
        },
        "returnOnEquity": {
            "raw": 0.101339996,
            "fmt": "10.13%"
        },
        "grossProfits": {
            "raw": 92407000000,
            "fmt": "92.41B",
            "longFmt": "92,407,000,000"
        },
        "freeCashflow": [],
        "operatingCashflow": [],
        "earningsGrowth": {
            "raw": 0.069,
            "fmt": "6.90%"
        },
        "revenueGrowth": {
            "raw": 0.04,
            "fmt": "4.00%"
        },
        "grossMargins": {
            "raw": 0,
            "fmt": "0.00%"
        },
        "ebitdaMargins": {
            "raw": 0,
            "fmt": "0.00%"
        },
        "operatingMargins": {
            "raw": 0.33514,
            "fmt": "33.51%"
        },
        "profitMargins": {
            "raw": 0.29790002,
            "fmt": "29.79%"
        },
        "financialCurrency": "USD"
    }
    
}

As you can see, also quickRatio and currentRatio are empty arrays of no type instead of CurrentRatio object nil. What can I do to solve this issue? Thank you!

I was thinking about computed properties, but it doesn't work. Is there any way to detect when parsing the data if the value is nil or not before attributing it to my variable? If I can do that, I will be able (for ebitda and similar cases):

If the value is not nil (in my case is not an empty array) to atribute to ebitda variable an Ebitda object from the json data.

If the value is nil(in my case an empty array of no type) to atribute to ebitda value a nil value of Ebitda? object.

I think that is the solution, I can't atribute to ebitda:Ebitda? an array.. I have to atribute a nil value of Ebitda?...but how can I detect that before atributing the value?

Thank you and I am here at any hour!

I have seen this issue before. I believe this is due to JS backend, which sometimes (and I don't really know why) sends empty array instead of the null. To distill your problem in simpler case: seems that you may get 2 different values for the same field:

let json1 = """
{
   "age": 2
}
""".data(using: .utf8)!

or, if value is nil , you are getting an empty array:

let json2 = """
{
   "age": []
}
""".data(using: .utf8)!

So if you just define your age as Int :

struct Obj: Codable {
    let age: Int?
}

it will work for json1 , but will crash for json2 .

Unfortunately it means you need to switch to "manually parse" such field, and because of it, the entire structure:

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: Keys.self)
    age = try? container.decode(Int.self, forKey: .age)
    // parse every field in the structure
}

But if you have many fields in your struct, it becomes very tedious. So a bit of a shortcut is to define a structure that parses it for you:

struct ValueOrEmptyArray<T: Codable>: Codable {
    
    let value: T?
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            value = try container.decode(T.self) // try to parse
        } catch {
            value = nil // failed parsing - assume it's nil
        }
    }
}

And so you can use it in your struct without doing parsing for each and every field:

struct Obj: Codable {
    let age: ValueOrEmptyArray<Int>
}

Of course it also means that to access a value of such field, you need to access age.value :

let decoded1 = try JSONDecoder().decode(Obj.self, from: json1)
print(decoded1.age.value) // 2
let decoded2 = try JSONDecoder().decode(Obj.self, from: json2) // throws an exception
print(decoded2.age.value) // nil

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM