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
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.
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 SomeEnum
s (#"["case1", "case2"]"#
, which also didn't work.
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