2

I want to parsing a JSON get from server but I have error i don't know why?!! this is my struct:

struct MyResponse:Decodable {
    let cats: [Cats]
}

struct Cats: Decodable {
    let id: Int
    let name: String
    let menu: [Cats]
    enum CodingKeys:String, CodingKey {
        case name
        case id
        case menu = "SubMenu"
    }
}

and create this extension :

extension MyResponse.Cats {
    init(from decoder: Decoder) throws {
        let valus = try decoder.container(keyedBy: CodingKeys.self)
        name = try valus.decode(String.self, forKey: .name)
        id = try valus.decode(Int.self, forKey: .id)
        menu = try valus.decodeIfPresent(String.self, forKey: .menu)
    } 
}

I don't know how to parse this json. This json is very important because this is category of store of store. and this is my json value :

{
"cats": [
    {
        "id": 15,
        "name": "کسب و کار ها",
        "menu": [
            {
                "id": 16,
                "name": "فروشگاهی",
                "menu": [
                    {
                        "id": 17,
                        "name": "ورزشی"
                    },
                    {
                        "id": 18,
                        "name": "نوشت افزار"
                    }
                ]
            },
            {
                "id": 19,
                "name": "خدماتی",
                "menu": ""
            }
        ]
    },

maybe in future menu now nil have sub menu how to handle if menu is nil or have some data ??

Edit: and this line in init :

menu = try valus.decodeIfPresent(String.self, forKey: .menu)

have this error :

Cannot assign value of type 'String?' to type '[MyResponse.Cats]'

1
  • Does your JSON really include a submenu entry as "menu": ""? If so, I’d fix whatever generates that JSON rather than programming around it. Commented Feb 14, 2018 at 0:21

3 Answers 3

1

The value for key menu can be

  • An array of Cat
  • An empty string
  • The key is missing

So you need to write an custom initializer which handles the cases. The easiest way is to decode an array of Cat. If it fails assign an empty array. Further you need an umbrella struct for the root object.

struct Root: Decodable {
     let cats : [Cat] // it's recommended to name structs in singular form.
}

struct Cat : Decodable {
    let id : Int
    let name : String
    let menu : [Cat]

    enum CodingKeys : String, CodingKey { case name, id, menu }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        name = try values.decode(String.self, forKey: .name)
        id = try values.decode(Int.self, forKey: .id)
        do {
            menu = try values.decode([Cat].self, forKey: .menu)
        } catch {
            menu = [Cat]()
        }
    }
}

Alternatively declare menu optional

let menu : [Cat]?

and assign nil if the value is not [Cat]

... } catch { menu = nil }

decodeIfPresent does not work because the value can be two different types.

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

Comments

0

JSON example includes an entry that says "menu": "", whereas your structure assumes it is either another Menu instance, null, or completely absent.

As vadian pointed out, if your submenus sometimes come back as "", you can write a custom init(from:) method that manually parsed the JSON, as he illustrated. But better would be to fix whatever generated that JSON, so that the "menu":"" was not present at all, or if it was, it would be "menu":null (note, no quotes). It's better to fix the original problem in the JSON, rather than writing cumbersome JSON parsing init(from:) to handle the problem.

Assuming you fix the JSON, there is, as others have noted, another problem. Your Menu structure defines a property called submenu, but your JSON doesn't use that key. It uses menu. So, you either can:

  1. Change the property name:

    struct Menu: Codable {
        let name: String
        let id: Int
        let menu: [Menu]?
    }
    

    or

  2. Use CodingKeys enumeration:

    struct Menu: Codable {
        let name: String
        let id: Int
        let submenu: [Menu]?
    
        enum CodingKeys: String, CodingKey {
            case name, id
            case submenu = "menu"
        }
    }
    

    or

  3. Change the JSON to use submenu key.


Assuming you fix the JSON, this demonstrates that you can parse it quite easily. This uses approach 2, shown above:

let data = """
    {
        "cats": [
            {
                "id": 15,
                "name": "کسب و کار ها",
                "menu": [
                    {
                        "id": 16,
                        "name": "فروشگاهی",
                        "menu": [
                            {
                                "id": 17,
                                "name": "ورزشی"
                            },
                            {
                                "id": 18,
                                "name": "نوشت افزار"
                            }
                        ]
                    },
                    {
                        "id": 19,
                        "name": "خدماتی"
                    }
                ]
            }
        ]
    }
    """.data(using: .utf8)!

struct Respons: Codable {
    let cats: [Menu]
}

struct Menu: Codable {
    let name: String
    let id: Int
    let submenu: [Menu]?

    enum CodingKeys: String, CodingKey {
        case name, id
        case submenu = "menu"
    }
}

do {
    let object = try JSONDecoder().decode(Respons.self, from: data)
    print(object)
} catch {
    print(error)
}

Comments

0

Just make your Cats structure conform to Codable protocol and add an optional array [Cats]?.

//: Playground - noun: a place where people can play

import Foundation

struct MyResponse: Codable {

    let cats: [Cats]
}

struct Cats: Codable {

    let id: Int
    let name: String
    let menu: [Cats]?
}

// create json mock by encoding

let cats3 = Cats(id: 3, name: "3", menu: nil)
let cats2 = Cats(id: 2, name: "2", menu: nil)
let cats1 = Cats(id: 1, name: "1", menu: [cats2, cats3])
let myResponse = MyResponse(cats: [cats1])
let json = try! JSONEncoder().encode(myResponse)

print(String(bytes: json, encoding: String.Encoding.utf8)) // Optional("{\"cats\":[{\"id\":1,\"name\":\"1\",\"menu\":[{\"id\":2,\"name\":\"2\"},{\"id\":3,\"name\":\"3\"}]}]}")

// create category data by decoding json (your actual question)

do {
    let myResponseAgain = try JSONDecoder().decode(MyResponse.self, from: json)
    for cats in myResponseAgain.cats {
        print(cats.id) // 1
        print(cats.name) // 1
        print(cats.menu) // Optional([__lldb_expr_30.Cats(id: 2, name: "2", menu: nil), __lldb_expr_30.Cats(id: 3, name: "3", menu: nil)])
        print(cats.menu![0].id) // 2
        print(cats.menu![0].name) // 2
        print(cats.menu![0].menu) // nil
        print(cats.menu![1].id) // 3
        print(cats.menu![1].name) // 3
        print(cats.menu![1].menu) // nil

    }
} catch {
    print("something went wrong")
}

1 Comment

thank for answer me i use this code but have error: "error : typeMismatch(Swift.Array<Any>"

Your Answer

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