Search code examples
swiftenumsdecodable

How do you make an enum decodable by its case name and not its raw value?


If I have an enum like this:

enum SomeEnum: String {
  case case1 = "raw value 1"
  case case2 = "raw value 2"
}

How would I make it conform to Decodable by using the case name (case1 and case2) instead of the raw values? For example, I would be able to use it like this:

let data = Data("\"case1\"".utf8)
let decodedEnum = try! JSONDecoder().decode(SomeEnum.self, from: data) // SomeEnum.case1

Edit

I added this to SomeEnum like what @Alexander said:

enum CodingKeys: String, CodingKey {
  case case1, case2
}

but I still got the error

The data couldn't be read because it isn't in the correct format.


Edit 2

I tried explicitly defining the raw values in the CodingKeys like what @Lutz said, but I got the same error. Just in case JSONDecoder didn't allow fragmented JSON, I tried using an array of SomeEnums (#"["case1", "case2"]"#, which also didn't work.


Solution

  • I looked into it and the problem here is that what you see in the JSON result is an encoded value, not a key. Consequently, adding CodingKeys won't help.

    A slightly complicated solution uses a custom protocol and a corresponding extension to achieve the goal.

    With that, you can declare:

        enum Test: String, CaseNameCodable {
            case one = "Number One"
            case two = "Number Two"
        }
    

    and it would do what you need.

    A complete working example is sketched below (works for me in a Playground in Xcode 11.2):

        import Foundation
    
        // A custom error type for decoding...
        struct CaseNameCodableError: Error {
            private let caseName: String
    
            init(_ value: String) {
                caseName = value
            }
    
            var localizedDescription: String {
                #"Unable to create an enum case named "\#(caseName)""#
            }
        }
    
        //
        // This is the interesting part:
        //
    
        protocol CaseNameCodable: Codable,  RawRepresentable ,  CaseIterable {}
    
        extension CaseNameCodable {
    
            init(from decoder: Decoder) throws {
                let container = try decoder.singleValueContainer()
                let value = try container.decode(String.self)
                guard let raw = Self.allCases.first(where: { $0.caseName == value })?.rawValue else { throw CaseNameCodableError(value) }
                self.init(rawValue: raw)!
            }
    
            func encode(to encoder: Encoder) throws {
                var container = encoder.singleValueContainer()
                try container.encode(caseName)
            }
    
            private var caseName: String {
                return "\(self)"
            }
        }
    
        //
        // Now you can use the protocol CaseNameCodable just like you
        // would use Codable (on RawRepresentable enums only)
        //
    
        enum Test: String, CaseNameCodable {
            case one = "Number One"
            case two = "Number Two"
        }
    
        // EXAMPLE:
    
        // Create a test value
        let testValue = Test.one
    
        // encode it and convert it to a String
        let jsonData = try! JSONEncoder().encode(testValue)
        let jsonString = String(data: jsonData, encoding: .utf8)!
    
        print (jsonString) // prints: "one"
    
        // decode the same data to produce a decoded enum instance
        let decodedTestValue = try JSONDecoder().decode(Test.self, from: jsonData)
    
        print(decodedTestValue.rawValue) // prints: Number One