6
\$\begingroup\$

Generally, the approach I've taken is to try calling container.decode from a decodable enum with its associated value returning the actual class. Each time the call to container.decode throws a different class is tried, from the class with the bigger number of member vars to the one with the smaller number of member vars.

This is how the deserialization is performed:

let rawAccount = try JSONDecoder().decode(RawAccount.self, from: data)
let account = rawAccount?.value

This is the decodable enum:

/// Nimiq account returned by the server. The especific type is in the associated value.
enum RawAccount : Decodable {
    case account(Account)
    case vesting(VestingContract)
    case htlc(HTLC)

    var value: Any {
        switch self {
        case .account(let value):
            return value
        case .vesting(let value):
            return value
        case .htlc(let value):
            return value
        }
    }

    private enum CodingKeys: String, CodingKey {
        case account, vestingContract, hashedTimeLockedContract
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            self = .htlc(try container.decode(HTLC.self))
        } catch {
            do {
                self = .vesting(try container.decode(VestingContract.self))
            } catch {
                self = .account(try container.decode(Account.self))
            }
        }
    }
}

These are the different classes that are returned in the associate value:

/// Normal Nimiq account object returned by the server.
public class Account: Decodable {
    /// Hex-encoded 20 byte address.
    public var id: String
    /// User friendly address (NQ-address).
    public var address: String
    /// Balance of the account (in smallest unit).
    public var balance: Int
    /// The account type associated with the account.
    public var type: AccountType
}

/// Vesting contract object returned by the server.
public class VestingContract : Account {
    /// Hex-encoded 20 byte address of the owner of the vesting contract.
    public var owner: String
    ///User-friendly address (NQ-address) of the owner of the vesting contract.
    public var ownerAddress: String
    /// The block that the vesting contracted commenced.
    public var vestingStart: Int
    /// The number of blocks after which some part of the vested funds is released.
    public var vestingStepBlocks: Int
    /// The amount (in smallest unit) released every vestingStepBlocks blocks.
    public var vestingStepAmount: Int
    /// The total amount (in the smallest unit) that was provided at the contract creation.
    public var vestingTotalAmount: Int

    private enum CodingKeys: String, CodingKey {
        case owner, ownerAddress, vestingStart, vestingStepBlocks, vestingStepAmount, vestingTotalAmount
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.owner = try container.decode(String.self, forKey: .owner)
        self.ownerAddress = try container.decode(String.self, forKey: .ownerAddress)
        self.vestingStart = try container.decode(Int.self, forKey: .vestingStart)
        self.vestingStepBlocks = try container.decode(Int.self, forKey: .vestingStepBlocks)
        self.vestingStepAmount = try container.decode(Int.self, forKey: .vestingStepAmount)
        self.vestingTotalAmount = try container.decode(Int.self, forKey: .vestingTotalAmount)
        try super.init(from: decoder)
    }
}

/// Hashed Timelock Contract object returned by the server.
public class HTLC : Account {
    /// Hex-encoded 20 byte address of the sender of the HTLC.
    public var sender: String
    /// User friendly address (NQ-address) of the sender of the HTLC.
    public var senderAddress: String
    /// Hex-encoded 20 byte address of the recipient of the HTLC.
    public var recipient: String
    /// User friendly address (NQ-address) of the recipient of the HTLC.
    public var recipientAddress: String
    /// Hex-encoded 32 byte hash root.
    public var hashRoot: String
    /// Hash algorithm.
    public var hashAlgorithm: Int
    /// Number of hashes this HTLC is split into.
    public var hashCount: Int
    /// Block after which the contract can only be used by the original sender to recover funds.
    public var timeout: Int
    /// The total amount (in smallest unit) that was provided at the contract creation.
    public var totalAmount: Int

    private enum CodingKeys: String, CodingKey {
        case sender, senderAddress, recipient, recipientAddress, hashRoot, hashAlgorithm, hashCount, timeout, totalAmount
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.sender = try container.decode(String.self, forKey: .sender)
        self.senderAddress = try container.decode(String.self, forKey: .senderAddress)
        self.recipient = try container.decode(String.self, forKey: .recipient)
        self.recipientAddress = try container.decode(String.self, forKey: .recipientAddress)
        self.hashRoot = try container.decode(String.self, forKey: .hashRoot)
        self.hashAlgorithm = try container.decode(Int.self, forKey: .hashAlgorithm)
        self.hashCount = try container.decode(Int.self, forKey: .hashCount)
        self.timeout = try container.decode(Int.self, forKey: .timeout)
        self.totalAmount = try container.decode(Int.self, forKey: .totalAmount)
        try super.init(from: decoder)
    }
}
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

The same question was answered on Reddit by u/Saladfork4

Here is the original answer:

Usually you'll have a "type" somewhere in the JSON that tells you how to decode a field with an inconsistent structure. In your case, it is probably type: AccountType? In that case, you'll usually want to decode that key first and then decode the rest based on that. Here's an example:

struct RawAccount: Decodable {

    enum CodingKeys: CodingKey {
        case type
    }

    let type: AccountType

    let account: Account

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        type = try container.decode(AccountType.self, forKey: .type)
    
        let accountContainer = try decoder.singleValueContainer()
        switch type {
        case .htlc:
            account = try accountContainer.decode(HTLC.self)
        case .vesting:
            account = try accountContainer.decode(VestingContract.self)
        default:
            account = try accountContainer.decode(Account.self)
        }
    }

}

public enum AccountType: String, Codable {

    case htlc = "HTLC"

    case vesting = "Vesting"

    case account = "Account"

}

// Example
do {
    let rawAccount = try decoder.decode(RawAccount.self, from: data)
    print(rawAccount.account)
} catch {
    print(error)
}

That way, you don't have to exhaustively try decoding it as every type--and you won't end up with a really big pyramid of code as you add more account types. If you prefer the enum with associated values instead of let account: Account, you can just add a field to RawAccount and use that instead after decoding within the switch.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.