'Creating custom CodingKey Object in Swift

I'm trying to do something a little custom with my Codable objects. My JSON objects use several types of tokens, so I'd like to make them type safe. To do that, I've created the following Codable classes:

class Token: Codable {
    let value: String

    init(_ value: String = "") {
        self.value = value
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()

        value = try container.decode(String.self)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()

        try container.encode(value)
    }
}

extension Token: Equatable { }
extension Token: Hashable { }

class UserToken: Token { }
class ProductToken: Token { }
// etc...

struct User: Codable {
    let token: UserToken
    let friends: [UserToken : User]
    // ...
}

JSON Objects:

// User
{
    "token":"12345",
    ...
}

This works great, except for the case where these tokens are used as keys in a dictionary like so:

// User
{
    "token":"12345",
    "friends":{
        "56789":{ // User
            "token":"56789",
            ...
        },
        "09876":{ // User
            "token":"09876",
            ...
        }
     }
}

To get this working, I've updated my Token class to conform to CodingKey (seems to be the right thing to do):

class Token: Codable, CodingKey {
    var stringValue: String {
        return value
    }

    var intValue: Int? {
        return Int(value)
    }

    required init?(stringValue: String) {
        value = stringValue
    }

    required init?(intValue: Int) {
        value = "\(intValue)"
    }

    // Plus above implementation
}

This does not seem to work properly though, failing with the following error. Looks like the JSONDecoder thinks it should be decoding an array instead of a dictionary... Is this a bug in Codable?

typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil))


Solution 1:[1]

This works.

struct User: Codable {
    let token: Token
    var friendsList: [Friend] {
        get { return friends.friends }
        set { friends.friends = newValue }
    }
    private var friends: FriendsList
    
    
    private struct FriendsList: Codable {
        var friends: [Friend]
        
        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            
            let dictionary = try container.decode([String: Friend].self)
            
            var friends = [Friend]()
            _ = dictionary.map { (_, value: Friend) in
                friends.append(value)
            }
            self.friends = friends
        }
        
        func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
            
            var dectionary = [String: Friend]()
            _ = friends.map { friend in
                dectionary[friend.token.value] = friend
            }
            try container.encode(dectionary)
        }
    }
}


struct Friend: Codable {
    let token: Token
}


class Token: Codable {
    let value: String

    init(_ value: String = "") {
        self.value = value
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()

        value = try container.decode(String.self)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()

        try container.encode(value)
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Ali Amin