如何使 Swift Codable 类型更加通用

How to make Swift Codable types more versatile

我目前正在使用处理公交车预测的 API。 JSON 有一个有趣的怪癖,它会返回一个特定停止的预测。当有多个停止预测时,JSON 看起来像这样:

...
"direction": {
            "prediction": [
                {
                    "affectedByLayover": "true",
                    "block": "241",
                    "dirTag": "loop",
                    "epochTime": "1571785998536",
                    "isDeparture": "false",
                    "minutes": "20",
                    "seconds": "1208",
                    "tripTag": "121",
                    "vehicle": "1698"
                },
                {
                    "affectedByLayover": "true",
                    "block": "241",
                    "dirTag": "loop",
                    "epochTime": "1571787798536",
                    "isDeparture": "false",
                    "minutes": "50",
                    "seconds": "3008",
                    "tripTag": "122",
                    "vehicle": "1698"
                },
                {
                    "affectedByLayover": "true",
                    "block": "241",
                    "dirTag": "loop",
                    "epochTime": "1571789598536",
                    "isDeparture": "false",
                    "minutes": "80",
                    "seconds": "4808",
                    "tripTag": "123",
                    "vehicle": "1698"
                }
            ],
            "title": "Loop"
        }
...

但是,当只有一个停止预测时,JSON 看起来像这样:

...
"direction": {
            "prediction": 
                {
                    "affectedByLayover": "true",
                    "block": "241",
                    "dirTag": "loop",
                    "epochTime": "1571785998536",
                    "isDeparture": "false",
                    "minutes": "20",
                    "seconds": "1208",
                    "tripTag": "121",
                    "vehicle": "1698"
                }
            "title": "Loop"
        }
...

请注意,"prediction" 不再位于数组内——这是我认为使用 Swift Codable 类型解码 JSON 时事情变得复杂的地方。对于 "direction" 和 "prediction"

,我的模型看起来像这样
struct BTDirection: Codable {
    let title: String!
    let stopTitle: String!
    let prediction: [BTPrediction]!
}

struct BTPrediction: Codable {
    let minutes: String!
    let vehicle: String!
}

基本上发生的事情是 prediction in BTDirection 正在寻找一个 BTPrediction 数组,但是在上面的第二种情况下,这不是一个数组,因此解码失败。如何使我的模型更灵活以适应数组或单个对象?理想情况下,在第二种情况下 prediction 仍然是单个 BTDirection 的数组。如有任何帮助,我们将不胜感激。

你可以试试

struct BTDirection:Codable {

    let title,stopTitle: String
    let prediction: [BTPrediction]

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        title = try container.decode(String.self, forKey: .title)
        stopTitle = try container.decode(String.self, forKey: .stopTitle)
        do {
            let res = try container.decode([BTPrediction].self, forKey: .prediction)
            prediction = res
        }
        catch { 
              let res = try container.decode(BTPrediction.self, forKey: .prediction)
              prediction = [res] 
        }  
    }
}

为了补充 Sh_Khan 的答案,如果您的 API 响应中有多个地方发生了此类事情,您可以将此自定义解码和编码提取到自定义包装器类型,因此您不必在任何地方重复它,例如:

/// Wrapper type that can be encoded/decoded to/from either
/// an array of `Element`s or a single `Element`.
struct ArrayOrSingleItem<Element> {
    private var elements: [Element]
}

extension ArrayOrSingleItem: Decodable where Element: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()

        do {
            // First try decoding the single value as an array of `Element`s.
            elements = try container.decode([Element].self)
        } catch {
            // If decoding as an array of `Element`s didn't work, try decoding
            // the single value as a single `Element`, and store it in an array.
            elements = try [container.decode(Element.self)]
        }
    }
}

extension ArrayOrSingleItem: Encodable where Element: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()

        if elements.count == 1, let element = elements.first {
            // If the wrapped array of `Element`s has exactly one `Element` 
            // in it, encode this as just that one `Element`.
            try container.encode(element)
        } else {
            // Otherwise, encode the wrapped array just as it is - an array
            // of `Element`s.
            try container.encode(elements)
        }
    }
}

// This lets you treat an `ArrayOrSingleItem` like a collection of elements.
// If you need the elements as type `Array<Element>`, just instantiate a new
// `Array` from your `ArrayOrSingleItem` like:
//     let directions: ArrayOrSingleItem<BTDirection> = ...
//     let array: [BTDirection] = Array(directions)
extension ArrayOrSingleItem: MutableCollection {
    subscript(position: Int) -> Element {
        get { elements[position] }
        set { elements[position] = newValue }
    }

    var startIndex: Int { elements.startIndex }
    var endIndex: Int { elements.endIndex }

    func index(after i: Int) -> Int {
        elements.index(after: i)
    }
}

// This lets you instantiate an `ArrayOrSingleItem` from an `Array` literal.
extension ArrayOrSingleItem: ExpressibleByArrayLiteral {
    init(arrayLiteral elements: Element...) {
        self.elements = elements
    }
}

然后您可以像这样声明您的 prediction(以及任何其他可能是数组或 API 响应中的单个项目的 属性):

struct BTDirection: Codable {
    let title: String?
    let stopTitle: String?
    let prediction: ArrayOrSingleItem<BTPrediction>?
}

@TylerTheCompiler 和@Sh_Khan 都为提供解决方案机制的解决方案提供了非常好的技术输入,但提供的代码会遇到给定 json 数据的一些实施问题:

  1. 发布的 JSON 中存在错误,将导致 codable 停止使用它 - 我怀疑这些只是复制和粘贴错误,但如果不是,您将遇到问题。
  2. 由于初始 direction 键 JSON 实际上有 3(或至少 2.5!)层嵌套。这将需要在 init(from:) 中展平,或者如下所示,需要一个临时结构以便于映射。在初始化程序中展平会更优雅,临时结构要快得多:-)
  3. CodingKeys,虽然很明显,但在之前的答案中没有定义,所以会导致编译 init(from:) 的错误
  4. JSON 中没有 stopTitle 字段,因此解码时会出错,除非将其视为可选字段。这里我把它当成一个具体的String,在解码的时候处理了;你可以把它变成 String? 然后解码器会处理它的缺失。

使用 "corrected" JSON(添加左大括号、缺少逗号等)以下代码将导入这两种情况。我还没有实现 arrayOrSingleItem 因为所有功劳都属于@TylerTheCompiler,但你可以很容易地把它放进去。

struct Direction: Decodable {
   let direction: BTDirection
}

struct BTDirection: Decodable {
   enum CodingKeys: String, CodingKey {
      case title
      case stopTitle
      case prediction
   }
   let prediction: [BTPrediction]
   let title: String
   let stopTitle: String

   init(from decoder: Decoder) throws {
      let container = try decoder.container(keyedBy: CodingKeys.self)
      do {
         prediction = try container.decode([BTPrediction].self, forKey: .prediction)
      } catch {
         let singlePrediction = try container.decode(BTPrediction.self, forKey: .prediction)
         prediction = [singlePrediction]
      }
      title = try container.decode(String.self, forKey: .title)
      stopTitle = try container.decodeIfPresent(String.self, forKey: .stopTitle) ?? "unnamed stop"
   }
}

struct BTPrediction: Decodable {
   let minutes: String
   let vehicle: String
}

然后实际解码 JSON 解码顶级方向类型

let data = json.data(using: .utf8)
if let data = data {
   do {
      let bus = try decoder.decode(Direction.self, from: data)
      // extract the BTDirection type from the temporary Direction type
      // and do something with the decoded data
   }catch {
      //handle error
   }
}

如果您不知道,JSON Validator 对 validating/correcting json 非常有用。