使用 Swift 解码异构数组 JSON 解码

Decode heterogeneous array JSON using Swift decodable

这是我正在尝试解码的JSON。 objectType 的值决定创建什么对象。

{
  "options": [
    {
      "objectType": "OptionTypeA",
      "label": "optionALabel1",
      "value": "optionAValue1"
    },
    {
      "objectType": "OptionTypeB",
      "label": "optionBLabel",
      "value": "optionBValue"
    },
    {
      "objectType": "OptionTypeA",
      "label": "optionALabel2",
      "value": "optionAValue2"
    }
  ]
}

假设我有这样定义的 2 个选项类型

public protocol OptionType {
  var label: String { get }
  var value: String { get }
}

struct OptionTypeA: Decodable {
  let label: String
  let value: String
  
  // and some others...
}

struct OptionTypeB: Decodable {
  let label: String
  let value: String
  
  // and some others...
}

struct Option: Decodable {
  let options: [OptionType]
  
  enum CodingKeys: String, CodingKey {
    case options
    case label
    case value
    case objectType
  }
  
  init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    var optionsContainer = try values.nestedUnkeyedContainer(forKey: .options)
    var options = [OptionType]()
    
    while !optionsContainer.isAtEnd {
      let itemContainer = try optionsContainer.nestedContainer(keyedBy: CodingKeys.self)
      
      switch try itemContainer.decode(String.self, forKey: .objectType) {
      // What should I do here so that I do not have to manually decode `OptionTypeA` and `OptionTypeB`?
      case "OptionTypeA": options.append() 
      case "OptionTypeB": options.append()
      default: fatalError("Unknown type")
      }
    }
    
    self.options = options
  }
}

我知道我可以手动解码 itemContainer 中的每个键并在开关盒中创建单独的选项类型对象。但我不想那样做。我怎样才能解码这些对象?

比通用属性协议更更快捷的方法是具有关联值的枚举。

Option 枚举首先解码 objectType——它甚至可以被解码为一个枚举——并根据值解码不同的结构。

enum OptionType : String, Decodable {
    case a = "OptionTypeA", b = "OptionTypeB"
}

struct Root : Decodable {
    let options : [Option]
}

enum Option : Decodable {
    private enum CodingKeys : String, CodingKey { case objectType }
    
    case typeA(OptionTypeA)
    case typeB(OptionTypeB)
    
    init(from decoder : Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let typeContainer = try decoder.singleValueContainer()
        let optionType = try container.decode(OptionType.self, forKey: .objectType)
        switch optionType {
            case .a: self = .typeA(try typeContainer.decode(OptionTypeA.self))
            case .b: self = .typeB(try typeContainer.decode(OptionTypeB.self))
        }
    }
}

struct OptionTypeA: Decodable {
  let label: String
  let value: String
  
  // and some others...
}

struct OptionTypeB: Decodable {
  let label: String
  let value: String
  
  // and some others...
}

let jsonString = """
{
  "options": [
    {
      "objectType": "OptionTypeA",
      "label": "optionALabel1",
      "value": "optionAValue1"
    },
    {
      "objectType": "OptionTypeB",
      "label": "optionBLabel",
      "value": "optionBValue"
    },
    {
      "objectType": "OptionTypeA",
      "label": "optionALabel2",
      "value": "optionAValue2"
    }
  ]
}
"""

let data = Data(jsonString.utf8)

do {
    let result = try JSONDecoder().decode(Root.self, from: data)
    for option in result.options {
        switch option {
            case .typeA(let optionTypeA): print(optionTypeA)
            case .typeB(let optionTypeB): print(optionTypeB)
        }
    }
} catch {
    print(error)
}