Reputation: 1177
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.
Upvotes: 75
Views: 35357
Reputation: 31
protocol UnknownCodable: RawRepresentable, Codable where RawValue: Codable {
static var unknown: Self { get }
}
extension UnknownCodable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let decodedValue = try container.decode(RawValue.self)
self = Self(rawValue: decodedValue) ?? Self.unknown
}
}
enum Type: String, UnknownCodable {
case text
case image
case document
case unknown
}
let types = try! JSONDecoder().decode([Type].self , from: Data(#"["text","image","sound"]"#.utf8))
print(types) // [text, image, unknown]
Upvotes: 3
Reputation: 413
To avoid decoding failure in case of wrong data type in JSON.
extension Type: Decodable {
init(from decoder: Decoder) {
do {
let rawValue = try decoder.singleValueContainer().decode(RawValue.self)
self = Type(rawValue: rawValue) ?? .unknown
} catch {
self = .unknown
}
}
}
or in case where RawValue
is String
extension Type: Decodable {
init(from decoder: Decoder) {
let rawValue = (try? decoder.singleValueContainer().decode(String.self)) ?? "unknown"
self = Type(rawValue: rawValue) ?? .unknown
}
}
For example: {"foo": "image", "bar": "cake", "baz": 42}
-> ["foo": .image, "bar": .unknown, "baz": .unknown]
Upvotes: 1
Reputation: 849
the following method will decode all types of enums with RawValue of type Decodable (Int, String, ..) and returns nil if it fails. This will prevent crashes caused by non-existent raw values inside the JSON response.
extension Decodable {
static func decode<T: RawRepresentable, R, K: CodingKey>(rawValue _: R.Type, forKey key: K, decoder: Decoder) throws -> T? where T.RawValue == R, R: Decodable {
let container = try decoder.container(keyedBy: K.self)
guard let rawValue = try container.decodeIfPresent(R.self, forKey: key) else { return nil }
return T(rawValue: rawValue)
}
}
enum Status: Int, Decodable {
case active = 1
case disabled = 2
}
struct Model: Decodable {
let id: String
let status: Status?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decodeIfPresent(String.self, forKey: .id)
status = try .decode(rawValue: Int.self, forKey: .status, decoder: decoder)
}
}
// status: -1 reutrns nil
// status: 2 returns .disabled
Upvotes: 1
Reputation: 20965
You can use this extension to encode / decode (this snippet supports Int an String RawValue type enums, but can be easy extended to fit other types)
extension NSCoder {
func encodeEnum<T: RawRepresentable>(_ value: T?, forKey key: String) {
guard let rawValue = value?.rawValue else {
return
}
if let s = rawValue as? String {
encode(s, forKey: key)
} else if let i = rawValue as? Int {
encode(i, forKey: key)
} else {
assert(false, "Unsupported type")
}
}
func decodeEnum<T: RawRepresentable>(forKey key: String, defaultValue: T) -> T {
if let s = decodeObject(forKey: key) as? String, s is T.RawValue {
return T(rawValue: s as! T.RawValue) ?? defaultValue
} else {
let i = decodeInteger(forKey: key)
if i is T.RawValue {
return T(rawValue: i as! T.RawValue) ?? defaultValue
}
}
return defaultValue
}
}
than use it
// encode
coder.encodeEnum(source, forKey: "source")
// decode
source = coder.decodeEnum(forKey: "source", defaultValue: Source.home)
Upvotes: 0
Reputation: 173
Let's start with a test case. We expect this to pass:
func testCodableEnumWithUnknown() throws {
enum Fruit: String, Decodable, CodableEnumWithUnknown {
case banana
case apple
case unknown
}
struct Container: Decodable {
let fruit: Fruit
}
let data = #"{"fruit": "orange"}"#.data(using: .utf8)!
let val = try JSONDecoder().decode(Container.self, from: data)
XCTAssert(val.fruit == .unknown)
}
Our protocol CodableEnumWithUnknown
denotes the support of the unknown
case that should be used by the decoder if an unknown value arises in the data.
And then the solution:
public protocol CodableEnumWithUnknown: Codable, RawRepresentable {
static var unknown: Self { get }
}
public extension CodableEnumWithUnknown where Self: RawRepresentable, Self.RawValue == String {
init(from decoder: Decoder) throws {
self = (try? Self(rawValue: decoder.singleValueContainer().decode(RawValue.self))) ?? Self.unknown
}
}
The trick is make your enum implement with the CodableEnumWithUnknown
protocol and add the unknown
case.
I favor this solution above using the .allCases.last!
implementation mentioned in other posts, because i find them a bit brittle, as they are not typechecked by the compiler.
Upvotes: 5
Reputation: 236305
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]
Upvotes: 199
Reputation: 159
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
}
}
Upvotes: 9
Reputation: 3098
@LeoDabus thanks for your answers. I modified them a bit to make a protocol for String enums that seems to work for me:
protocol CodableWithUnknown: Codable {}
extension CodableWithUnknown where Self: RawRepresentable, Self.RawValue == String {
init(from decoder: Decoder) throws {
do {
try self = Self(rawValue: decoder.singleValueContainer().decode(RawValue.self))!
} catch {
if let unknown = Self(rawValue: "unknown") {
self = unknown
} else {
throw error
}
}
}
}
Upvotes: 2
Reputation: 19
Add this extension and set YourEnumName
.
extension <#YourEnumName#>: Codable {
public init(from decoder: Decoder) throws {
self = try <#YourEnumName#>(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown
}
}
Upvotes: 1
Reputation: 8739
Here's an alternative based on nayem's answer that offers a slightly more streamlined syntax by using optional binding of the inner RawValues
initialization:
enum MyEnum: Codable {
case a, b, c
case other(name: String)
private enum RawValue: String, Codable {
case a = "a"
case b = "b"
case c = "c"
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let decodedString = try container.decode(String.self)
if let value = RawValue(rawValue: decodedString) {
switch value {
case .a:
self = .a
case .b:
self = .b
case .c:
self = .c
}
} else {
self = .other(name: decodedString)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .a:
try container.encode(RawValue.a)
case .b:
try container.encode(RawValue.b)
case .c:
try container.encode(RawValue.c)
case .other(let name):
try container.encode(name)
}
}
}
If you are certain that all your existing enum case names match the underlying string values they represent, you could streamline RawValue
to:
private enum RawValue: String, Codable {
case a, b, c
}
...and encode(to:)
to:
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if let rawValue = RawValue(rawValue: String(describing: self)) {
try container.encode(rawValue)
} else if case .other(let name) = self {
try container.encode(name)
}
}
Here's a practical example of using this, e.g., you want to model SomeValue
that has a property you want to model as an enum:
struct SomeValue: Codable {
enum MyEnum: Codable {
case a, b, c
case other(name: String)
private enum RawValue: String, Codable {
case a = "a"
case b = "b"
case c = "letter_c"
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let decodedString = try container.decode(String.self)
if let value = RawValue(rawValue: decodedString) {
switch value {
case .a:
self = .a
case .b:
self = .b
case .c:
self = .c
}
} else {
self = .other(name: decodedString)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .a:
try container.encode(RawValue.a)
case .b:
try container.encode(RawValue.b)
case .c:
try container.encode(RawValue.c)
case .other(let name):
try container.encode(name)
}
}
}
}
let jsonData = """
[
{ "value": "a" },
{ "value": "letter_c" },
{ "value": "c" },
{ "value": "Other value" }
]
""".data(using: .utf8)!
let decoder = JSONDecoder()
if let values = try? decoder.decode([SomeValue].self, from: jsonData) {
values.forEach { print($0.value) }
let encoder = JSONEncoder()
if let encodedJson = try? encoder.encode(values) {
print(String(data: encodedJson, encoding: .utf8)!)
}
}
/* Prints:
a
c
other(name: "c")
other(name: "Other value")
[{"value":"a"},{"value":"letter_c"},{"value":"c"},{"value":"Other value"}]
*/
Upvotes: 7
Reputation: 7585
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:
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)
}
}
}
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)
}
}
}
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.
Upvotes: 16
Reputation: 14030
You have to implement the init(from decoder: Decoder) throws
initializer and check for a valid value:
struct SomeStruct: Codable {
enum SomeType: String, Codable {
case text
case image
case document
case profile
case sign
case inputDate = "input_date"
case inputText = "input_text"
case inputNumber = "input_number"
case inputOption = "input_option"
case unknown
}
var someType: SomeType
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
someType = (try? values.decode(SomeType.self, forKey: .someType)) ?? .unknown
}
}
Upvotes: 3