如何在Swift中使枚举可解码?

237
enum PostType: Decodable {

    init(from decoder: Decoder) throws {

        // What do i put here?
    }

    case Image
    enum CodingKeys: String, CodingKey {
        case image
    }
}
我该输入什么才能完成这个任务? 另外,假设我将 case 改成了这样:
case image(value: Int)

如何使它符合可解码协议(Decodable)?

这是我完整的代码(但不起作用)

let jsonData = """
{
    "count": 4
}
""".data(using: .utf8)!
        
        do {
            let decoder = JSONDecoder()
            let response = try decoder.decode(PostType.self, from: jsonData)
            
            print(response)
        } catch {
            print(error)
        }
    }
}

enum PostType: Int, Codable {
    case count = 4
}

还有,它将如何处理像这样的枚举?

enum PostType: Decodable {
    case count(number: Int)
}
10个回答

389

很简单,只需使用隐式分配的StringInt原始值即可。

enum PostType: Int, Codable {
    case image, blob
}

image 被编码为 0,而 blob 则被编码为 1

或者

enum PostType: String, Codable {
    case image, blob
}

image 被编码为 "image",而 blob 则被编码为 "blob"


这是一个使用它的简单示例:

enum PostType : Int, Codable {
    case count = 4
}

struct Post : Codable {
    var type : PostType
}

let jsonString = "{\"type\": 4}"

let jsonData = Data(jsonString.utf8)

do {
    let decoded = try JSONDecoder().decode(Post.self, from: jsonData)
    print("decoded:", decoded.type)
} catch {
    print(error)
}

更新

iOS 13.3+和macOS 15.1+已经允许对单个JSON值进行编码或解码,这些值被称为“片段”,不需要放在集合类型中。

let jsonString = "4"

let jsonData = Data(jsonString.utf8)
do {
    let decoded = try JSONDecoder().decode(PostType.self, from: jsonData)
    print("decoded:", decoded) // -> decoded: count
} catch {
    print(error)
}

在 Swift 5.5+ 中,甚至可以不需要任何额外的代码,对带有关联值的枚举类型进行编码和解码。这些值被映射到一个字典中,每个关联值必须指定一个参数标签。

enum Rotation: Codable {
    case zAxis(angle: Double, speed: Int)
}

let jsonString = #"{"zAxis":{"angle":90,"speed":5}}"#

let jsonData = Data(jsonString.utf8)
do {
    let decoded = try JSONDecoder().decode(Rotation.self, from: jsonData)
    print("decoded:", decoded)
} catch {
    print(error)
}

1
我尝试了你建议的代码,但它不起作用。我已经编辑了我的代码以显示我正在尝试解码的JSON。 - swift nub
14
一个枚举类型不能单独进行编码或解码,它必须嵌入在一个结构体中。我添加了一个示例。 - vadian
我将标记此为正确。但上面的问题还有最后一部分没有回答。如果我的枚举看起来像这样呢?(编辑如上所示) - swift nub
2
如果您正在使用带有关联类型的枚举,则必须编写自定义编码和解码方法。请阅读《编码和解码自定义类型》(https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types)。 - vadian
4
关于“枚举类型无法单独进行编码/解码”的问题,这个问题似乎在iOS 13.3中得到了解决。我在iOS 13.3iOS 12.4.3上进行了测试,它们表现不同。在iOS 13.3下,枚举类型可以被独立地进行编码/解码。 - AechoLiu

172

如何使关联类型的枚举符合Codable

这个答案与@Howard Lovatt的类似,但避免创建一个PostTypeCodableForm结构体,而是使用EncoderDecoder上的KeyedEncodingContainer类型由苹果提供作为属性,从而减少样板代码。

enum PostType: Codable {
    case count(number: Int)
    case title(String)
}

extension PostType {

    private enum CodingKeys: String, CodingKey {
        case count
        case title
    }

    enum PostTypeCodingError: Error {
        case decoding(String)
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        if let value = try? values.decode(Int.self, forKey: .count) {
            self = .count(number: value)
            return
        }
        if let value = try? values.decode(String.self, forKey: .title) {
            self = .title(value)
            return
        }
        throw PostTypeCodingError.decoding("Whoops! \(dump(values))")
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case .count(let number):
            try container.encode(number, forKey: .count)
        case .title(let value):
            try container.encode(value, forKey: .title)
        }
    }
}

这段代码在Xcode 9b3上运行良好。

import Foundation // Needed for JSONEncoder/JSONDecoder

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let decoder = JSONDecoder()

let count = PostType.count(number: 42)
let countData = try encoder.encode(count)
let countJSON = String.init(data: countData, encoding: .utf8)!
print(countJSON)
//    {
//      "count" : 42
//    }

let decodedCount = try decoder.decode(PostType.self, from: countData)

let title = PostType.title("Hello, World!")
let titleData = try encoder.encode(title)
let titleJSON = String.init(data: titleData, encoding: .utf8)!
print(titleJSON)
//    {
//        "title": "Hello, World!"
//    }
let decodedTitle = try decoder.decode(PostType.self, from: titleData)

我喜欢这个答案!顺便一提,这个例子也在a post on objc.io about making Either codable中提到。 - Ky -

55

如果Swift遇到未知的枚举值,它将会抛出一个.dataCorrupted错误。如果数据来自服务器,它随时可能向您发送未知的枚举值(例如服务器端的bug,API版本中添加了新类型并且您希望应用程序的早期版本能够优雅地处理此情况等),因此最好做好准备,并以“防御性编程”的方式安全地解码枚举。

以下是如何进行示例,包括是否具有相关值。

    enum MediaType: Decodable {
       case audio
       case multipleChoice
       case other
       // case other(String) -> we could also parametrise the enum like that

       init(from decoder: Decoder) throws {
          let label = try decoder.singleValueContainer().decode(String.self)
          switch label {
             case "AUDIO": self = .audio
             case "MULTIPLE_CHOICES": self = .multipleChoice
             default: self = .other
             // default: self = .other(label)
          }
       }
    }

如何在一个封闭的结构体中使用它:

    struct Question {
       [...]
       let type: MediaType

       enum CodingKeys: String, CodingKey {
          [...]
          case type = "type"
       }


   extension Question: Decodable {
      init(from decoder: Decoder) throws {
         let container = try decoder.container(keyedBy: CodingKeys.self)
         [...]
         type = try container.decode(MediaType.self, forKey: .type)
      }
   }

1
谢谢,你的回答更容易理解了。 - DazChong
1
这个答案对我也有帮助,谢谢。如果让你的枚举继承自String,它可以得到改进,这样你就不需要在字符串上进行切换了。 - Gobe
简单明了的回答。谢谢,这个方法完美地解决了问题! - Christian Gossain

53
为了扩展@Toka的回答,您也可以向枚举添加原始可表示值,并使用默认可选构造函数来构建枚举而无需使用switch
enum MediaType: String, Decodable {
  case audio = "AUDIO"
  case multipleChoice = "MULTIPLE_CHOICES"
  case other

  init(from decoder: Decoder) throws {
    let label = try decoder.singleValueContainer().decode(String.self)
    self = MediaType(rawValue: label) ?? .other
  }
}

它可以使用自定义协议进行扩展,该协议允许重构构造函数:

protocol EnumDecodable: RawRepresentable, Decodable {
  static var defaultDecoderValue: Self { get }
}

extension EnumDecodable where RawValue: Decodable {
  init(from decoder: Decoder) throws {
    let value = try decoder.singleValueContainer().decode(RawValue.self)
    self = Self(rawValue: value) ?? Self.defaultDecoderValue
  }
}

enum MediaType: String, EnumDecodable {
  static let defaultDecoderValue: MediaType = .other

  case audio = "AUDIO"
  case multipleChoices = "MULTIPLE_CHOICES"
  case other
}

还可以轻松地进行扩展,以便在指定无效的枚举值时引发错误,而不是默认值。带有此更改的Gist在此处可用:https://gist.github.com/stephanecopin/4283175fabf6f0cdaf87fef2a00c8128
该代码是使用Swift 4.1/Xcode 9.3编译和测试的。


3
这就是我所寻找的答案。 - Nathan Hosselton
感谢保持简单 :) - Pedro Trujillo

8
一种更加简洁的@proxpero响应变体是将解码器表述为:
public init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    guard let key = values.allKeys.first else { throw err("No valid keys in: \(values)") }
    func dec<T: Decodable>() throws -> T { return try values.decode(T.self, forKey: key) }

    switch key {
    case .count: self = try .count(dec())
    case .title: self = try .title(dec())
    }
}

func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    switch self {
    case .count(let x): try container.encode(x, forKey: .count)
    case .title(let x): try container.encode(x, forKey: .title)
    }
}

这样可以让编译器彻底验证各种情况,并且不会忽略当编码值与键的预期值不匹配时的错误消息。

我同意这更好。 - proxpero
整洁的解决方案,我喜欢它。 - SomaMan

7
实际上,以上的回答都非常好,但是它们缺少一些细节,这些细节对于许多人在一个不断发展的客户端/服务器项目中所需要的是必要的。我们开发一个应用程序,而我们的后端不断地随着时间的推移而发展,这意味着一些枚举案例将会发生变化。因此,我们需要一种能够解码包含未知案例的枚举数组的枚举解码策略。否则,包含该数组的对象的解码将会失败。
我所做的事情非常简单:
enum Direction: String, Decodable {
    case north, south, east, west
}

struct DirectionList {
   let directions: [Direction]
}

extension DirectionList: Decodable {

    public init(from decoder: Decoder) throws {

        var container = try decoder.unkeyedContainer()

        var directions: [Direction] = []

        while !container.isAtEnd {

            // Here we just decode the string from the JSON which always works as long as the array element is a string
            let rawValue = try container.decode(String.self)

            guard let direction = Direction(rawValue: rawValue) else {
                // Unknown enum value found - ignore, print error to console or log error to analytics service so you'll always know that there are apps out which cannot decode enum cases!
                continue
            }
            // Add all known enum cases to the list of directions
            directions.append(direction)
        }
        self.directions = directions
    }
}

额外福利:隐藏实现 > 将其变成一个集合

隐藏实现细节总是一个好主意。为此,您只需要多写一点代码。技巧在于将DirectionsList符合Collection规范,使内部的list数组私有化:

struct DirectionList {

    typealias ArrayType = [Direction]

    private let directions: ArrayType
}

extension DirectionList: Collection {

    typealias Index = ArrayType.Index
    typealias Element = ArrayType.Element

    // The upper and lower bounds of the collection, used in iterations
    var startIndex: Index { return directions.startIndex }
    var endIndex: Index { return directions.endIndex }

    // Required subscript, based on a dictionary index
    subscript(index: Index) -> Element {
        get { return directions[index] }
    }

    // Method that returns the next index when iterating
    func index(after i: Index) -> Index {
        return directions.index(after: i)
    }
}

您可以阅读 John Sundell 的这篇博客文章,了解有关符合自定义集合的更多信息:https://medium.com/@johnsundell/creating-custom-collections-in-swift-a344e25d0bb0


6

你可以做你想做的事情,但它有点复杂 :(

import Foundation

enum PostType: Codable {
    case count(number: Int)
    case comment(text: String)

    init(from decoder: Decoder) throws {
        self = try PostTypeCodableForm(from: decoder).enumForm()
    }

    func encode(to encoder: Encoder) throws {
        try PostTypeCodableForm(self).encode(to: encoder)
    }
}

struct PostTypeCodableForm: Codable {
    // All fields must be optional!
    var countNumber: Int?
    var commentText: String?

    init(_ enumForm: PostType) {
        switch enumForm {
        case .count(let number):
            countNumber = number
        case .comment(let text):
            commentText = text
        }
    }

    func enumForm() throws -> PostType {
        if let number = countNumber {
            guard commentText == nil else {
                throw DecodeError.moreThanOneEnumCase
            }
            return .count(number: number)
        }
        if let text = commentText {
            guard countNumber == nil else {
                throw DecodeError.moreThanOneEnumCase
            }
            return .comment(text: text)
        }
        throw DecodeError.noRecognizedContent
    }

    enum DecodeError: Error {
        case noRecognizedContent
        case moreThanOneEnumCase
    }
}

let test = PostType.count(number: 3)
let data = try JSONEncoder().encode(test)
let string = String(data: data, encoding: .utf8)!
print(string) // {"countNumber":3}
let result = try JSONDecoder().decode(PostType.self, from: data)
print(result) // count(3)

有趣的黑客攻击 - Roman Filippov

1
这里有很多好的方法,但我还没有看到有人讨论过具有多个值的枚举,尽管可以从示例中推断出来 - 也许有人可以找到这个的用途:
import Foundation

enum Tup {
  case frist(String, next: Int)
  case second(Int, former: String)
  
  enum TupType: String, Codable {
    case first
    case second
  }
  enum CodingKeys: String, CodingKey {
    case type
    
    case first
    case firstNext
    
    case second
    case secondFormer
  }
  
}

extension Tup: Codable {
  init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    let type = try values.decode(TupType.self, forKey: .type)
    switch type {
    case .first:
      let str = try values.decode(String.self, forKey: .first)
      let next = try values.decode(Int.self, forKey: .firstNext)
      self = .frist(str, next: next)
    case .second:
      let int = try values.decode(Int.self, forKey: .second)
      let former = try values.decode(String.self, forKey: .secondFormer)
      self = .second(int, former: former)
    }
  }
  
  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    
    switch self {
    case .frist(let str, next: let next):
      try container.encode(TupType.first, forKey: .type)
      try container.encode(str, forKey: .first)
      try container.encode(next, forKey: .firstNext)
    case .second(let int, former: let former):
      try container.encode(TupType.second, forKey: .type)
      try container.encode(int, forKey: .second)
      try container.encode(former, forKey: .secondFormer)
    }
    
  }
}

let example1 = Tup.frist("123", next: 90)
do {
  let encoded = try JSONEncoder().encode(example1)
  print(encoded)
  
  let decoded = try JSONDecoder().decode(Tup.self, from: encoded)
  print("decoded 1 = \(decoded)")
}
catch {
  print("errpr = \(error.localizedDescription)")
}


let example2 = Tup.second(10, former: "dantheman")

do {
  let encoded = try JSONEncoder().encode(example2)
  print(encoded)
  
  let decoded = try JSONDecoder().decode(Tup.self, from: encoded)
  print("decoded 2 = \(decoded)")
}
catch {
  print("errpr = \(error.localizedDescription)")
}


1

特点

  • 简单易用。在Decodable实例中只需一行代码:例如 let enum: DecodableEnum<AnyEnum>
  • 使用标准映射机制进行解码:JSONDecoder().decode(Model.self, from: data)
  • 处理未知数据的情况(例如,如果接收到意外数据,则映射Decodable对象不会失败)
  • 处理/传递映射或解码错误

细节

  • Xcode 12.0.1 (12A7300)
  • Swift 5.3

解决方案

import Foundation

enum DecodableEnum<Enum: RawRepresentable> where Enum.RawValue == String {
    case value(Enum)
    case error(DecodingError)

    var value: Enum? {
        switch self {
        case .value(let value): return value
        case .error: return nil
        }
    }

    var error: DecodingError? {
        switch self {
        case .value: return nil
        case .error(let error): return error
        }
    }

    enum DecodingError: Error {
        case notDefined(rawValue: String)
        case decoding(error: Error)
    }
}

extension DecodableEnum: Decodable {
    init(from decoder: Decoder) throws {
        do {
            let rawValue = try decoder.singleValueContainer().decode(String.self)
            guard let layout = Enum(rawValue: rawValue) else {
                self = .error(.notDefined(rawValue: rawValue))
                return
            }
            self = .value(layout)
        } catch let err {
            self = .error(.decoding(error: err))
        }
    }
}

使用示例

enum SimpleEnum: String, Codable {
    case a, b, c, d
}

struct Model: Decodable {
    let num: Int
    let str: String
    let enum1: DecodableEnum<SimpleEnum>
    let enum2: DecodableEnum<SimpleEnum>
    let enum3: DecodableEnum<SimpleEnum>
    let enum4: DecodableEnum<SimpleEnum>?
}

let dictionary: [String : Any] = ["num": 1, "str": "blablabla", "enum1": "b", "enum2": "_", "enum3": 1]

let data = try! JSONSerialization.data(withJSONObject: dictionary)
let object = try JSONDecoder().decode(Model.self, from: data)
print("1. \(object.enum1.value)")
print("2. \(object.enum2.error)")
print("3. \(object.enum3.error)")
print("4. \(object.enum4)")

enter image description here


为什么不将其作为属性包装器,如果情况不存在,则只抛出解码错误呢? 这种技术会带走可编码协议(和枚举)为您执行的所有隐含数据验证。要点:https://gist.github.com/DouweBos/2e1ece3230b2f61f3285e488c72cbee4 - Houwert

1
这是一个简单的示例,展示如何在Swift中使枚举可解码。 示例JSON:
[
    {
        "title": "1904",
        "artist": "The Tallest Man on Earth",
        "year": "2012",
        "type": "hindi"
    },
    {
        "title": "#40",
        "artist": "Dave Matthews",
        "year": "1999",
        "type": "english"
    },
    {
        "title": "40oz to Freedom",
        "artist": "Sublime",
        "year": "1996",
        "type": "english"
    },
    {
        "title": "#41",
        "artist": "Dave Matthews",
        "year": "1996",
        "type": "punjabi"
    }
]

模型结构:

struct Song: Codable {
    
    public enum SongType: String, Codable {
        case hindi = "hindi"
        case english = "english"
        case punjabi = "punjabi"
        case tamil = "tamil"
        case none = "none"
    }
    
    let title: String
    let artist: String
    let year: String
    let type: SongType?
}

现在,您可以解析 JSON 文件并将数据解析为以下的歌曲数组:
func decodeJSON() {
    do {
        // creating path from main bundle and get data object from the path
        if let bundlePath = Bundle.main.path(forResource: "sample", ofType: "json"),
           let jsonData = try String(contentsOfFile: bundlePath).data(using: .utf8) {
            
            // decoding an array of songs
            let songs = try JSONDecoder().decode([Song].self, from: jsonData)
            
            // printing the type of song
            songs.forEach { song in
                print("Song type: \(song.type?.rawValue ?? "")")
            }
        }
    } catch {
        print(error)
    }
}

如有任何疑问,请在下方评论。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接