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.

1

2 Answers 2

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.

Sign up to request clarification or add additional context in comments.

6 Comments

I'll try that! Where should I put that init?
@YumaTechnicalInc. Make your MyModel/MyProduct conform to Decodable protocol and declare this method there. reference implementation.
I put it inside my model. What is 'MemberKeys' & 'stringMember'? Is this solution for any/all members?
'MemberKeys' & 'stringMember' are just examples of any properties your model has. Check the reference I put in the comment above.
@YumaTechnicalInc. Updated the answer.
|
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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.