[SOLVED] Swift: decoding an array of mixed values

Issue

I have a Decodable struct in Swift, with an array that might contain ints or a string representation of a double:

{
    "result": {
        "XXBTZUSD": [
            [
                1616662740,
                "52591.9",
                "52599.9",
                "52591.8",
                "52599.9",
                "52599.1",
                "0.11091626",
                5
            ]
        ],
        "last": 1616662920
    }
}

My struct looks like this:

struct OHLCData: Decodable {
    var pair: [[Double]]
    var last: Int
    
    private struct CodingKeys: CodingKey {
        var intValue: Int?
        var stringValue: String
        init?(stringValue: String) {  self.stringValue = stringValue  }
        init?(intValue: Int) {
            self.stringValue = String(intValue)
            self.intValue = intValue
        }
        static let last = CodingKeys(stringValue: "last")!
        static func makeKey(name: String) -> CodingKeys {
            return CodingKeys(stringValue: name)!
        }
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        last = try container.decode(Int.self, forKey: .last)
        let key = container.allKeys.first(where: { $0.stringValue != "last" } )?.stringValue
        pair = try container.decode([[Double]].self, forKey: .makeKey(name: key!))
    }
    
}

It falls over at run time because the double type in [[Double]] doesn’t correspond to what might be an int in the array.

I tried to create a typealias but I get:

// Non-protocol, non-class type 'String' cannot be used within a protocol-constrained type
// Non-protocol, non-class type 'Double' cannot be used within a protocol-constrained type
typealias PairType = String & Double

How can I work around this?

Solution

You could decode it using something like:

enum Either<A, B> {
    case left(A)
    case right(B)
}

extension Either: Codable where A: Codable, B: Codable {
    struct NeitherError: Error {}

    func encode(to encoder: Encoder) throws {
        switch self {
        case let .left(a):
            try a.encode(to: encoder)
        case let .right(b):
            try b.encode(to: encoder)
        }
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let a = try? container.decode(A.self) {
            self = .left(a)
        }
        else if let b = try? container.decode(B.self) {
            self = .right(b)
        }
        else {
            throw NeitherError()
        }
    }
}

let input = """
[
               1616662740,
               "52591.9",
               "52599.9",
               "52591.8",
               "52599.9",
               "52599.1",
               "0.11091626",
               5
           ]
"""

let data = input.data(using: .utf8)!
let things = try! JSONDecoder().decode([Either<Int, String>].self, from: data)

You might want to split the Codable constraint into two extensions for Encodable and Decodable, and you might want to make a custom type which is StringifiedDouble which only decodes if the thing is a String AND is a stringified representation of a Double, e.g.

struct StringifiedDouble: Codable {
    var value: Double
    struct TypeError: Error {}

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let str = try container.decode(String.self)

        guard let value = Double(str) else {
            throw TypeError()
        }

        self.value = value
    }
}

Answered By – Shadowrun

Answer Checked By – David Goodson (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *