Swift 4 JSON Decodable with multidimensional and multitype array

后端 未结 5 1442
天命终不由人
天命终不由人 2020-11-30 13:32
{
\"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\",\"         


        
相关标签:
5条回答
  • 2020-11-30 13:50

    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"]]
    
    0 讨论(0)
  • 2020-11-30 13:51

    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
    
    0 讨论(0)
  • 2020-11-30 14:00

    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

    0 讨论(0)
  • 2020-11-30 14:09

    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"
        }
    }
    
    0 讨论(0)
  • 2020-11-30 14:16

    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
    }
    

    }

    0 讨论(0)
提交回复
热议问题