0

To get JSON from a website and turn it into an object is fairly simple using swift 4:

class func getJSON(completionHandler: @escaping (MyModel) -> Void)
{
    let myUrl = "https://example.com/whatever"
    if let myUrl = URL(string: myUrl)
    {
        URLSession.shared.dataTask(with: myUrl)
        { (data, response, err) in
            if let data = data
            {
                do
                {
                    let myData = try JSONDecoder().decode(MyModel.self, from: data)
                    completionHandler(myData)
                }
                catch let JSONerr
                {
                    print("Error: \(JSONerr)")
                }
            }
            return
        }.resume()
    }
}

MyModel is a data model:

struct MyModel
{
    products: [MyProduct]
}
struct MyProduct
{
     id: Int

...


I use this to GET from my WebService and it works well for most JSON structures.

However, I facing trouble with this complex JSON object. (By complex I mean too long to post here, so I hope you can figure-out such a pattern. Also the JSON object it has many nested arrays, etc.)

Eg.

{
    "products" : [
    {
        "name" : "abc"
        "colors" : [
        {
             "outside" : [
             {
                  "main" : "blue"
             }]
        }]
        "id" :

...

    },
    {
        "name" : "xyzzy"
        "colors" : [
        {
             "outside" : [
             {
                  "main" : "blue"
             }]
        }]
        "id" :

...

    }]
}

(This may not be valid JSON - it is a simple extract of a larger part.)


  • The app crashes "...Expected to decode String but found a number instead."!
  • So I change the model to use a 'String' in place of the 'Int'.
  • Again it crashes, saying "...Expected to decode Int but found a string/data instead."!
  • So I change the model back, place an 'Int' in place of the 'String'. (The cycle repeats.)

It seems the value in question is sometimes an Int and sometimes a String.

This NOT only happens with a certain key. I know of at least five other similar cases in this JSON.

So that means that I may get another error for another key, if a solution was only for that specific key. I would not be surprised to find many other cases as well.

QUESTION: How can I properly decode the JSON to my object, where the type of its elements can be either an Int or a String?

I want a solution that will either apply to all Model members or try convert a value to a String type if Int fails. Since I don't know which other keys will as fail.

  • Possible duplicate of [What Is Preventing My Conversion From String to Int When Decoding Using Swift 4’s Codable?](https://stackoverflow.com/questions/48513704/what-is-preventing-my-conversion-from-string-to-int-when-decoding-using-swift-4) – Larme Feb 26 '18 at 14:22

2 Answers2

1

You can use if lets to handle unpredictable values:

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: MemberKeys.self)
    if let memberValue = try? container.decode([String].self, forKey: .member){
        stringArrayMember = memberValue
    }
    else if let str = try? container.decode(String.self, forKey: .member){
        stringMember = str
    }
    else if let int = try? container.decode(Int.self, forKey: .member){
        intMember = int
    }
 }

Or if it's a specific case of String vs Int and you'd like the same variable to handle the values, then something like:

init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: MemberKeys.self)
        if let str = try? container.decode(String.self, forKey: .member){
            stringMember = str
        }
        else if let int = try? container.decode(Int.self, forKey: .member){
            stringMember = String(int)
        }
     }

Edit

Your MyProduct will now look like:

struct MyProduct: Decodable {
    var id: String?
    var someOtherProperty: String?

    enum MemberKeys: String, CodingKey {
        case id
        case someOtherProperty
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: MemberKeys.self)

        someOtherProperty = try? container.decode(String.self, forKey: .someOtherProperty)

        // Problematic property which can be String/Int
        if let str = try? container.decode(String.self, forKey: .id){
            id = str
        }
        else if let int = try? container.decode(Int.self, forKey: .id){
            id = String(int)
        }
    }
}

Hope this helps.

justintime
  • 352
  • 3
  • 11
0

This wasn't the problem that the error message gave!

All I needed to do to fix the problem was to employ CodingKeys.

I was hoping to avoid this since the data structure (JSON) had lots of members. But this fixed the problem.

Now an example of my model:

struct Product
{
    let name: String
    let long_name_value: String

...

    enum MemberKeys: String, CodingKey
    {
        case name
        case longNameValue = "long_name_value"

...

    }
}

I guess the reason is swift doesn't like snake case (eg. "long_name_value"), so I needed to convert it to camel case (eg."longNameValue"). Then the errors disappeared.