Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift 4 JSON Decodable with multidimensional and multitype array

{
"values":[
[1,1,7,"Azuan Child","Anak Azuan","12345","ACTIVE","Morning",7,12,"2017-11-09 19:45:00"],
[28,1,0,"Azuan Child2","Amran","123456","ACTIVE","Evening",1,29,"2017-11-09 19:45:00"]
]
}

Ok, this is my json format that i received from the server

Right now i want to decode it into my struct but still have no luck on it.

struct ChildrenTable: Decodable {
    var values: [[String]]?
}

And my caller method on URLSession look like this

URLSession.shared.dataTask(with: request) { (data, response, err) in
        guard let data = data else { return }

        let dataAsString = String(data: data, encoding: .utf8)
        print(dataAsString)

        do {
            let children  = try
                JSONDecoder().decode(ChildrenTable.self, from: data)
                print (children)
        } catch let jsonErr {
            print ("Error serializing json: ", jsonErr)
        }
    }.resume()

And the error that i got are

Error serializing json:  
typeMismatch(Swift.String, Swift.DecodingError.Context(codingPath: [Vito_Parent.ChildrenTable.(CodingKeys in _1B826CD7D9609504747BED0EC0B7D3B5).values, Foundation.(_JSONKey in _12768CA107A31EF2DCE034FD75B541C9)(stringValue: "Index 0", intValue: Optional(0)), 
Foundation.(_JSONKey in _12768CA107A31EF2DCE034FD75B541C9)(stringValue: "Index 0", intValue: Optional(0))], 
debugDescription: "Expected to decode String but found a number instead.", underlyingError: nil))

I know there's an int in the array and i only cast String for the values var values: [[String]]? ( the reason why this error popup), but i simply cannot use any multidimensional array or tuples in my structs since it follow the protocol of Decodable.

I also cannot convert the data into dictionary since it will throw error "Expected to decode Dictionary but found array instead"

Any ideas on solving this problem? i tried casting string type on data but still no luck...

p/s: if all the json format are in string type, there would be no problem, but i dont have the permission on changing that since i call it from API.

like image 598
Alan md noh Avatar asked Apr 12 '26 15:04

Alan md noh


2 Answers

As you said, your json array is multi-type but you are trying to decode all into String. Default conformance of String to Decodable does not allow that. The only solution comes into my mind is to introduce new type.

struct IntegerOrString: Decodable {
    var value: Any

    init(from decoder: Decoder) throws {
        if let int = try? Int(from: decoder) {
            value = int
            return
        }

        value = try String(from: decoder)
    }
}

struct ChildrenTable: Decodable {
    var values: [[IntegerOrString]]?
}

Run online

like image 109
Orkhan Alikhanov Avatar answered Apr 15 '26 03:04

Orkhan Alikhanov


Observe that the inner arrays in the JSON have a patterned sequence of types, and we know what that sequence is. The types in the inner arrays are in a patterned sequence: 3 Ints, 5 Strings, 2 Ints, and something probably intended as a Date. Clearly, in the minds of the JSON designers, each of these 11 elements has a fixed and known meaning.

This means that we can pick up the 11 elements one by one, manually, by dumpster-diving and decoding the entire JSON expression manually.

The arrays have mixed types, and Swift doesn't like that, so we will have to express them as an array of Any (or of AnyObject); but we can obtain them as themselves, rather than having to wrap them up in an artificial intermediate struct.

By the way, if you know what the meaning of each element is, then instead of an array of Any, you can decode the inner array into a struct with 11 named properties that express what each element signifies. That would be a cleaner result, but I have not used it because I do not know the meanings of the 11 values.

Here we go:

struct S : Decodable {
    var values : [[Any]]
    enum CodingKeys : String, CodingKey {
        case values
    }
    init(from decoder: Decoder) throws {
        // get the dictionary
        let con = try! decoder.container(keyedBy: CodingKeys.self)
        // get the "values" array of array
        var con2 = try! con.nestedUnkeyedContainer(forKey: CodingKeys.values)
        var bigarr = [[Any]]()
        for _ in 0..<con2.count! {
            // get a nested array
            var con3 = try! con2.nestedUnkeyedContainer()
            // decode all the elements of the nested array
            var arr = [Any]()
            arr.append(try! con3.decode(Int.self))
            arr.append(try! con3.decode(Int.self))
            arr.append(try! con3.decode(Int.self))
            arr.append(try! con3.decode(String.self))
            arr.append(try! con3.decode(String.self))
            arr.append(try! con3.decode(String.self))
            arr.append(try! con3.decode(String.self))
            arr.append(try! con3.decode(String.self))
            arr.append(try! con3.decode(Int.self))
            arr.append(try! con3.decode(Int.self))
            arr.append(try! con3.decode(String.self))
            bigarr.append(arr)
        }
        // all done! finish initialization
        self.values = bigarr
    }
}

let result = try! JSONDecoder().decode(S.self, from: jdata)
print(result.values)
// [[1, 1, 7, "Azuan Child", "Anak Azuan", "12345", "ACTIVE",
// "Morning", 7, 12, "2017-11-09 19:45:00"], 
// [28, 1, 0, "Azuan Child2", "Amran", "123456", "ACTIVE", 
// "Evening", 1, 29, "2017-11-09 19:45:00"]]
like image 39
matt Avatar answered Apr 15 '26 04:04

matt