Codable enum with default case in Swift 4
I have defined an enum
as follows:
enum Type: String, Codable {
case text = "text"
case image = "image"
case document = "document"
case profile = "profile"
case sign = "sign"
case inputDate = "input_date"
case inputText = "input_text"
case inputNumber = "input_number"
case inputOption = "input_option"
case unknown
}
that maps a JSON string property. The automatic serialization and deserialization works fine, but I found that if a different string is encountered, the deserialization fails.
Is it possible to define an unknown
case that maps any other available case?
This can be very useful, since this data comes from a RESTFul API that, maybe, can change in the future.
You can extend your Codable
Type and assign a default value in case of failure:
enum Type: String {
case text,
image,
document,
profile,
sign,
inputDate = "input_date",
inputText = "input_text" ,
inputNumber = "input_number",
inputOption = "input_option",
unknown
}
extension Type: Codable {
public init(from decoder: Decoder) throws {
self = try Type(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown
}
}
edit/update:
Xcode 11.2 • Swift 5.1 or later
Create a protocol that defaults to last case of a CaseIterable & Decodable
enumeration:
protocol CaseIterableDefaultsLast: Decodable & CaseIterable & RawRepresentable
where RawValue: Decodable, AllCases: BidirectionalCollection { }
extension CaseIterableDefaultsLast {
init(from decoder: Decoder) throws {
self = try Self(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? Self.allCases.last!
}
}
Playground testing:
enum Type: String, CaseIterableDefaultsLast {
case text, image, document, profile, sign, inputDate = "input_date", inputText = "input_text" , inputNumber = "input_number", inputOption = "input_option", unknown
}
let types = try! JSONDecoder().decode([Type].self , from: Data(#"["text","image","sound"]"#.utf8)) // [text, image, unknown]
You can drop the raw type for your Type
and make unknown case that handles associated value. But this comes at a cost. You somehow need the raw values for your cases. Inspired from this and this SO answers I came up with this elegant solution to your problem.
To be able to store the raw values, we will maintain another enum, but as private:
enum Type {
case text
case image
case document
case profile
case sign
case inputDate
case inputText
case inputNumber
case inputOption
case unknown(String)
// Make this private
private enum RawValues: String, Codable {
case text = "text"
case image = "image"
case document = "document"
case profile = "profile"
case sign = "sign"
case inputDate = "input_date"
case inputText = "input_text"
case inputNumber = "input_number"
case inputOption = "input_option"
// No such case here for the unknowns
}
}
Move the encoding
& decoding
part to extensions:
Decodable part:
extension Type: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
// As you already know your RawValues is String actually, you decode String here
let stringForRawValues = try container.decode(String.self)
// This is the trick here...
switch stringForRawValues {
// Now You can switch over this String with cases from RawValues since it is String
case RawValues.text.rawValue:
self = .text
case RawValues.image.rawValue:
self = .image
case RawValues.document.rawValue:
self = .document
case RawValues.profile.rawValue:
self = .profile
case RawValues.sign.rawValue:
self = .sign
case RawValues.inputDate.rawValue:
self = .inputDate
case RawValues.inputText.rawValue:
self = .inputText
case RawValues.inputNumber.rawValue:
self = .inputNumber
case RawValues.inputOption.rawValue:
self = .inputOption
// Now handle all unknown types. You just pass the String to Type's unknown case.
// And this is true for every other unknowns that aren't defined in your RawValues
default:
self = .unknown(stringForRawValues)
}
}
}
Encodable part:
extension Type: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .text:
try container.encode(RawValues.text)
case .image:
try container.encode(RawValues.image)
case .document:
try container.encode(RawValues.document)
case .profile:
try container.encode(RawValues.profile)
case .sign:
try container.encode(RawValues.sign)
case .inputDate:
try container.encode(RawValues.inputDate)
case .inputText:
try container.encode(RawValues.inputText)
case .inputNumber:
try container.encode(RawValues.inputNumber)
case .inputOption:
try container.encode(RawValues.inputOption)
case .unknown(let string):
// You get the actual String here from the associated value and just encode it
try container.encode(string)
}
}
}
Examples:
I just wrapped it in a container structure(because we'll be using JSONEncoder/JSONDecoder) as:
struct Root: Codable {
let type: Type
}
For values other than unknown case:
let rootObject = Root(type: Type.document)
do {
let encodedRoot = try JSONEncoder().encode(rootObject)
do {
let decodedRoot = try JSONDecoder().decode(Root.self, from: encodedRoot)
print(decodedRoot.type) // document
} catch {
print(error)
}
} catch {
print(error)
}
For values with unknown case:
let rootObject = Root(type: Type.unknown("new type"))
do {
let encodedRoot = try JSONEncoder().encode(rootObject)
do {
let decodedRoot = try JSONDecoder().decode(Root.self, from: encodedRoot)
print(decodedRoot.type) // unknown("new type")
} catch {
print(error)
}
} catch {
print(error)
}
I put the example with local objects. You can try with your REST API response.
enum Type: String, Codable, Equatable {
case image
case document
case unknown
public init(from decoder: Decoder) throws {
guard let rawValue = try? decoder.singleValueContainer().decode(String.self) else {
self = .unknown
return
}
self = Type(rawValue: rawValue) ?? .unknown
}
}