[英]Codable enum with default case in Swift 4
I have defined an enum
as follows:我定义了一个
enum
如下:
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.映射一个 JSON 字符串属性。 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?是否可以定义一个映射任何其他可用案例的
unknown
案例?
This can be very useful, since this data comes from a RESTFul API that, maybe, can change in the future.这可能非常有用,因为这些数据来自 RESTFul API,未来可能会发生变化。
You can extend your Codable
Type and assign a default value in case of failure:您可以扩展您的
Codable
类型并在失败时分配默认值:
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 Xcode 11.2 • Swift 5.1 或更高版本
Create a protocol that defaults to last case of a CaseIterable & Decodable
enumeration:创建一个默认为
CaseIterable & Decodable
枚举的最后一个案例的协议:
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.您可以删除
Type
的原始类型,并制作处理关联值的未知大小写。 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.
受到这个和这个SO 答案的启发,我想出了这个优雅的解决方案来解决你的问题。
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:将
encoding
和decoding
部分移至扩展:
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:我只是将它包装在一个容器结构中(因为我们将使用 JSONEncoder/JSONDecoder)作为:
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.
您可以尝试使用 REST API 响应。
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
}
}
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:这是基于nayem的答案的替代方案,它通过使用内部
RawValues
初始化的可选绑定提供了一种稍微简化的语法:
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:如果您确定所有现有的枚举案例名称都与它们所代表的基础字符串值匹配,您可以将
RawValue
简化为:
private enum RawValue: String, Codable {
case a, b, c
}
...and encode(to:)
to: ...并
encode(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, eg, you want to model SomeValue
that has a property you want to model as an enum:这是一个使用它的实际示例,例如,您想要对具有要建模为枚举的属性的
SomeValue
建模:
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"}]
*/
You have to implement the init(from decoder: Decoder) throws
initializer and check for a valid value:您必须实现
init(from decoder: Decoder) throws
initializer 并检查有效值:
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
}
}
Add this extension and set YourEnumName
.添加此扩展并设置
YourEnumName
。
extension <#YourEnumName#>: Codable {
public init(from decoder: Decoder) throws {
self = try <#YourEnumName#>(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown
}
}
@LeoDabus thanks for your answers. @LeoDabus 感谢您的回答。 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
}
}
}
}
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.我们的协议
CodableEnumWithUnknown
表示支持unknown
情况,如果数据中出现未知值,解码器应该使用这种情况。
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.诀窍是使您的枚举使用
CodableEnumWithUnknown
协议实现并添加unknown
情况。
I favor this solution above using the .allCases.last!
我喜欢上面使用
.allCases.last!
implementation mentioned in other posts, because i find them a bit brittle, as they are not typechecked by the compiler.其他帖子中提到的实现,因为我发现它们有点脆弱,因为编译器没有对它们进行类型检查。
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)您可以使用此扩展进行编码/解码(此代码段支持 Int 和 String RawValue 类型的枚举,但可以轻松扩展以适应其他类型)
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)
the following method will decode all types of enums with RawValue of type Decodable (Int, String, ..) and returns nil if it fails.以下方法将使用 Decodable (Int, String, ..) 类型的 RawValue 解码所有类型的枚举,如果失败则返回 nil。 This will prevent crashes caused by non-existent raw values inside the JSON response.
这将防止由于 JSON 响应中不存在的原始值导致的崩溃。
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
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.