【问题标题】:How to make Swift Codable types more versatile如何让 Swift Codable 类型更加通用
【发布时间】:2020-02-19 03:21:02
【问题描述】:

我目前正在使用一个处理公交车预测的 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"
        }
...

请注意,“预测”不再位于数组中——我认为使用 Swift Codable 类型解码 JSON 时事情会变得复杂。对于“方向”和“预测”,我的模型看起来像这样

struct BTDirection: Codable {
    let title: String!
    let stopTitle: String!
    let prediction: [BTPrediction]!
}

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

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

【问题讨论】:

    标签: ios json swift codable


    【解决方案1】:

    你可以试试

    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] 
            }  
        }
    }
    

    【讨论】:

      【解决方案2】:

      为了补充 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>?
      }
      

      【讨论】:

        【解决方案3】:

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

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

        使用“更正”的 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解码顶级Direction类型

        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 对于验证/纠正 json 非常有用。

        【讨论】:

          猜你喜欢
          • 2019-03-11
          • 1970-01-01
          • 1970-01-01
          • 2021-12-29
          • 1970-01-01
          • 2019-06-26
          • 2018-06-26
          • 2022-08-21
          相关资源
          最近更新 更多