Swift and iOS dev with Igor
The ride of the iOS developer

JSON

SO

Cases

Case 1

To decode JSON that represents the array of heterogeneous elements as a struct with fields that hold those elements, use UnkeyedDecodingContainer and iterate over fields with func decode(). Each call of decode() moves iteration to the next element, make sure to decode all fields by checking the count or isAtEnd properties.

import Foundation

/// `Payload`:
///
///     [
///         0,
///         "XBT/USD"
///     ]
///
struct Message {
    let channelID: Int
    let pair: String
}

extension Message: Decodable {
    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()

        let channelID = try container.decode(Int.self)
        let pair = try container.decode(String.self)

        guard container.isAtEnd else {
            throw ThereIsMoreToDecodeError()
        }

        self.init(channelID: channelID, pair: pair)
    }

    struct ThereIsMoreToDecodeError: Error {}
}

Run to test

let payload = """
    [
        0,
        "XBT/USD"
    ]
"""

let data = Data(payload.utf8)
let decoder = JSONDecoder()
let message = try decoder.decode(Message.self, from: data)

Case 2

To decode heterogeneous array where elements have common and different parts use struct with enum field for changing part and non-enum for common part.

import Foundation

/// `Payload`
///
///     [
///         {
///             "channelID": 10001,
///             "channelName": "ticker",
///             "event": "subscriptionStatus"
///         },
///         {
///             "errorMessage": "Subscription depth not supported",
///             "channelName": "ticker",
///             "event": "subscriptionStatus"
///         }
///     ]
struct Message {
    typealias Name = String
    typealias Event = String

    let oneOf: OneOf
    let name: Name
    let event: Event
}

extension Message: Decodable {
    enum CodingKeys: String, CodingKey {
        case name, event
    }

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

        let keyedContainer = try decoder.container(keyedBy: CodingKeys.self)
        let name = try keyedContainer.decode(Name.self, forKey: .name)
        let event = try keyedContainer.decode(Event.self, forKey: .event)

        self.init(oneOf: oneOf, name: name, event: event)
    }
}

enum OneOf: Decodable {
    case errorMessage(ErrorMessage)
    case channelID(ChannelID)

    struct ErrorMessage: Decodable {
        let errorMessage: String
    }

    struct ChannelID: Decodable {
        let channelID: Int
    }

    enum CodingKeys: String, CodingKey {
        case errorMessage, channelID
    }

    init(from decoder: Decoder) throws {
        do {
            let errorMessage = try ErrorMessage(from: decoder)
            self = .errorMessage(errorMessage)
        } catch {
            let channelID = try ChannelID(from: decoder)
            self = .channelID(channelID)
        }
    }
}

Run to test

let payload = """
     [
         {
             "channelID": 10001,
             "name": "ticker",
             "event": "subscriptionStatus"
         },
         {
             "errorMessage": "Subscription depth not supported",
             "name": "ticker",
             "event": "subscriptionStatus"
         }
     ]
"""

let data = Data(payload.utf8)
let decoder = JSONDecoder()
let messages = try decoder.decode([Message].self, from: data)
Published on: Feb 7, 2022
Tagged with: