简体   繁体   中英

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.

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

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"]]

This answer is build on top of the answer by @Orkhan Alikhanov

Since the values are Int or String , we can better represent them with an enum in place of Any .

The following code can be pasted into Playground

JSON

So let's start with the JSON

let data = """
{
    "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"]
    ]
}
""".data(using: .utf8)!

Data Model

Now we can define our model (which will be Decodable )

enum IntOrString: Decodable {

    case int(Int)
    case string(String)

    init(from decoder: Decoder) throws {

        if let string = try? decoder.singleValueContainer().decode(String.self) {
            self = .string(string)
            return
        }

        if let int = try? decoder.singleValueContainer().decode(Int.self) {
            self = .int(int)
            return
        }

        throw IntOrStringError.intOrStringNotFound
    }

    enum IntOrStringError: Error {
        case intOrStringNotFound
    }
}

As you can see we are explicitly saying that each value will be an Int or a String .

Response

And of course we need our Response type.

struct Response: Decodable {
    var values: [[IntOrString]]
}

Decoding

Now we can safely decode the JSON

if let response = try? JSONDecoder().decode(Response.self, from: data) {
    let values = response.values

    for value in values {
        for intOrString in value {
            switch intOrString {
            case .int(let int): print("It's an int: \(int)")
            case .string(let string): print("It's a string: \(string)")
            }
        }
    }
}

Output

It's an int: 1
It's an int: 1
It's an int: 7
It's a string: Azuan Child
It's a string: Anak Azuan
It's a string: 12345
It's a string: ACTIVE
It's a string: Morning
It's an int: 7
It's an int: 12
It's a string: 2017-11-09 19:45:00
It's an int: 28
It's an int: 1
It's an int: 0
It's a string: Azuan Child2
It's a string: Amran
It's a string: 123456
It's a string: ACTIVE
It's a string: Evening
It's an int: 1
It's an int: 29
It's a string: 2017-11-09 19:45:00

Solution

public struct UncertainValue<T: Decodable, U: Decodable>: Decodable {
    public var tValue: T?
    public var uValue: U?

    public var value: Any? {
        return tValue ?? uValue
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        tValue = try? container.decode(T.self)
        uValue = try? container.decode(U.self)
        if tValue == nil && uValue == nil {
            //Type mismatch
            throw DecodingError.typeMismatch(type(of: self), DecodingError.Context(codingPath: [], debugDescription: "The value is not of type \(T.self) and not even \(U.self)"))
        }

    }
}

Example

{
"results": [{
        "name": "Gala",
        "age": 1,
        "type": "Pug"
    }, {
        "name": "Keira",
        "age": "7",
        "type": "Collie Rough"
    }]
}

Usage

struct Dog: Decodable, CustomStringConvertible {
    var name: String
    var age: UncertainValue<Int, String>
    var type: String

    var description: String {
        return "\(name) is a lovely \(type) of \(age.value!) years old"
    }
}

I've tried use your solution on my project, and its work like a charm. Below i've modified a bit so it can be use for single string, single array, and multidimensional array

struct TripModel: Decodable {
var tx_result: Any
var columns: [Any]
var values: [[Any]]

enum CodingKeys : String, CodingKey {
    case tx_result
    case columns
    case values
}

init(from decoder: Decoder) throws {
    var bigarr = [[Any]]()
    var arrColumn = [Any]()
    // get the dictionary
    let con = try! decoder.container(keyedBy: CodingKeys.self)

    let conResult = try! con.decode(String.self, forKey: CodingKeys.tx_result)

    var conColumns = try! con.nestedUnkeyedContainer(forKey: CodingKeys.columns)
    //print(String(describing: conColumns.count))

    // get the "values" array of array
    var con2 = try! con.nestedUnkeyedContainer(forKey: CodingKeys.values)

    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(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(Int.self))
        arr.append(try! con3.decode(Int.self))
        arr.append(try! con3.decode(Double.self))
        arr.append(try! con3.decode(String.self))
        bigarr.append(arr)
    }

        arrColumn.append(try! conColumns.decode(String.self))
        arrColumn.append(try! conColumns.decode(String.self))
        arrColumn.append(try! conColumns.decode(String.self))
        arrColumn.append(try! conColumns.decode(String.self))
        arrColumn.append(try! conColumns.decode(String.self))
        arrColumn.append(try! conColumns.decode(String.self))
        arrColumn.append(try! conColumns.decode(String.self))
        arrColumn.append(try! conColumns.decode(String.self))
        arrColumn.append(try! conColumns.decode(String.self))
        arrColumn.append(try! conColumns.decode(String.self))
        arrColumn.append(try! conColumns.decode(String.self))

    // all done! finish initialization
    self.tx_result = conResult
    self.columns = arrColumn
    self.values = bigarr
}

}

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