Swift 4 JSON Decodable with multidimensional and multitype array
Solution 1:
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
Solution 2:
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"]]
Solution 3:
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 aString
.
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 4:
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"
}
}