Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Codable enum with multiple keys and associated values

I've seen answers on how to make an enum conform to Codable when all cases have an associated value, but I'm not clear on how to mix enums that have cases with and without associated values:

??? How can I use multiple variations of the same key for a given case?

??? How do I encode/decode cases with no associated value?

enum EmployeeClassification : Codable, Equatable {

case aaa
case bbb
case ccc(Int) // (year)

init?(rawValue: String?) {
    guard let val = rawValue?.lowercased() else {
        return nil
    }
    switch val {
        case "aaa", "a":
            self = .aaa
        case "bbb":
            self = .bbb
        case "ccc":
            self = .ccc(0)
        default: return nil
    }
}

// Codable
private enum CodingKeys: String, CodingKey {
    case aaa // ??? how can I accept "aaa", "AAA", and "a"?
    case bbb
    case ccc
}

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    if let value = try? container.decode(Int.self, forKey: .ccc) {
        self = .ccc(value)
        return
    }
    // ???
    // How do I decode the cases with no associated value?
}
func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    switch self {
    case .ccc(let year):
        try container.encode(year, forKey: .ccc)
    default:
        // ???
        // How do I encode cases with no associated value?
    }
}
}
like image 226
GoldenJoe Avatar asked Feb 03 '23 17:02

GoldenJoe


2 Answers

Use the assumed raw string values of the init method as (string) value of the enum case

enum EmployeeClassification : Codable, Equatable {
    
    case aaa
    case bbb
    case ccc(Int) // (year)
    
    init?(rawValue: String?) {
        guard let val = rawValue?.lowercased() else {
            return nil
        }
        switch val {
        case "aaa", "a":
            self = .aaa
        case "bbb":
            self = .bbb
        case "ccc":
            self = .ccc(0)
        default: return nil
        }
    }
    
    // Codable
    private enum CodingKeys: String, CodingKey { case aaa, bbb, ccc }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let value = try? container.decode(Int.self, forKey: .ccc) {
            self = .ccc(value)
        } else if let aaaValue = try? container.decode(String.self, forKey: .aaa), ["aaa", "AAA", "a"].contains(aaaValue) {
            self = .aaa
        } else if let bbbValue = try? container.decode(String.self, forKey: .bbb), bbbValue == "bbb" {
            self = .bbb
        } else {
            throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath, debugDescription: "Data doesn't match"))
        }
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case .aaa: try container.encode("aaa", forKey: .aaa)
        case .bbb: try container.encode("bbb", forKey: .bbb)
        case .ccc(let year): try container.encode(year, forKey: .ccc)
        }
    }
}

The Decoding Error is quite generic. You can throw more specific errors for each CodingKey

like image 190
vadian Avatar answered Feb 17 '23 00:02

vadian


Starting with Swift 5.5, enums with associated values gained the ability of having automatic conformance to Codable. See this swift-evolution proposal for more details about the implementation details.

So, this is enough for your enum:

enum EmployeeClassification : Codable, Equatable {
    case aaa
    case bbb
    case ccc(Int) // (year)

No more CodingKeys, no more init(from:), or encode(to:)

like image 34
Cristik Avatar answered Feb 17 '23 01:02

Cristik