【问题标题】:Decoding arbitrary json field dynamically in Swift在 Swift 中动态解码任意 json 字段
【发布时间】:2018-12-09 22:19:36
【问题描述】:

TL;DR

有没有一种方法可以让我使用JSONDecoder 并编写一个函数,该函数将从给定的 json 给定的指定可解码类型的字段值中读出?


成像我有以下json:

{
   "product":{
      "name":"PR1",
      "price":20
   },
   "employee":{
      "lastName":"Smith",
      "department":"IT",
      "manager":"Anderson"
   }
}

我有 2 个Decodable 结构:

struct Product: Decodable {
    var name: String
    var price: Int
}

struct Employee: Decodable {
    var lastName: String
    var department: String
    var manager: String
}

我想写一个函数

func getValue<T:Decodable>(from json: Data, field: String) -> T { ... }

所以我可以这样称呼它:

let product: Product = getValue(from: myJson, field: "product")
let employee: Employee = getValue(from: myJson, field: "employee")

这可能与JSONDecoder 或我应该与JSONSerialization 混淆,首先读出给定json 的“子树”,然后将其传递给解码器? swift 中似乎不允许在泛型函数中定义结构。

【问题讨论】:

    标签: json swift nsjsonserialization jsondecoder


    【解决方案1】:

    Decodable 假定您在设计时知道启用静态类型所需的一切。你想要的越有活力,你就必须变得越有创造力。在这种情况下,定义通用编码键结构非常方便:

    /// A structure that holds no fixed key but can generate dynamic keys at run time
    struct GenericCodingKeys: CodingKey {
        var stringValue: String
        var intValue: Int?
    
        init?(stringValue: String) { self.stringValue = stringValue }
        init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
        static func makeKey(_ stringValue: String) -> GenericCodingKeys { return self.init(stringValue: stringValue)! }
        static func makeKey(_ intValue: Int) -> GenericCodingKeys { return self.init(intValue: intValue)! }
    }
    
    /// A structure that retains just the decoder object so we can decode dynamically later
    fileprivate struct JSONHelper: Decodable {
        let decoder: Decoder
    
        init(from decoder: Decoder) throws {
            self.decoder = decoder
        }
    }
    
    func getValue<T: Decodable>(from json: Data, field: String) throws -> T {
        let helper = try JSONDecoder().decode(JSONHelper.self, from: json)
        let container = try helper.decoder.container(keyedBy: GenericCodingKeys.self)
        return try container.decode(T.self, forKey: .makeKey(field))
    }
    
    let product: Product = try getValue(from: json, field: "product")
    let employee: Employee = try getValue(from: json, field: "employee")
    

    【讨论】:

      【解决方案2】:

      我首先要说Code Different's answer 是一个可行且很好的答案,但是如果您寻求一种不同的方式来做这件事,尽管在表面下工作方式大致相同,我有一个替代解决方案,使用主要组件代码不同的答案,导致下面的代码。主要区别之一是,一个JSONDecoder 在同一个JSON 上被重用,对于你提取的每个struct,使用这个。

      我也会推荐这些:


      /// Conforming to this protocol, makes the type decodable using the JSONContainer class
      /// You can use `Decodable` instead.
      protocol JSONContainerCodable: Codable {
      
          /// Returns the name that the type is recognized with, in the JSON.
          /// This is overridable in types conforming to the protocol.
          static var containerIdentifier: String { get }
      
          /// Defines whether or not the type's container identifier is lowercased.
          /// Defaults to `true`
          static var isLowerCased: Bool { get }
      }
      
      extension JSONContainerCodable {
      
          static var containerIdentifier: String {
              let identifier = String(describing: self)
              return !isLowerCased ? identifier : identifier.lowercased()
          }
      
          static var isLowerCased: Bool {
              return true
          }
      }
      
      struct Product: JSONContainerCodable {
      
          var name:  String
          var price: Int
      }
      
      struct Employee: JSONContainerCodable {
      
          var lastName:   String
          var department: String
          var manager:    String
      }
      
      /// This class is simply a wrapper around JSONDecoder
      class JSONContainerDecoder: Decodable {
      
          private struct AnyCodingKeys: CodingKey {
      
              var stringValue: String
              var intValue: Int?
      
              init?(intValue: Int) {
                  self.intValue = intValue
                  self.stringValue = "\(intValue)"
              }
      
              init?(stringValue: String) {
                  self.stringValue = stringValue
              }
      
              init(_ string: String) {
                  stringValue = string
              }
          }
      
          private let decoder: JSONDecoder
          private let container: KeyedDecodingContainer<AnyCodingKeys>
      
          /// Overrides the initializer as specified in `Decodable`.
          required init(from decoder: Decoder) throws {
              self.decoder = JSONDecoder()
              self.container = try decoder.container(keyedBy: AnyCodingKeys.self)
          }
      
          /// Factory initializer. Swift (4.2) currently doesn't support overriding the parentheses operator.
          static func decoding(_ data: Data, with decoder: JSONDecoder = JSONDecoder()) throws -> JSONContainerDecoder {
              return try decoder.decode(JSONContainerDecoder.self, from: myJSON)
          }
      
          /// Gets the given type from the JSON, based on its field/container identifier, and decodes it. Assumes there exists only one type with the given field/container identifier, in the JSON.
          func get<T: JSONContainerCodable>(_ type: T.Type, field: String? = nil) throws -> T {
              return try container.decode(T.self, forKey: AnyCodingKeys(field ?? T.containerIdentifier))
          }
      
          /// Short version of the decode getter above; assumes the variable written to already has its type defined.
          func get<T: JSONContainerCodable>(field: String? = nil) throws -> T {
              return try get(T.self, field: field)
          }
      }
      

      let myJSON = """
      {
          "product": {
              "name": "PR1",
              "price": 20
          },
          "employee": {
              "lastName": "Smith",
              "department": "IT",
              "manager": "Anderson"
          }
      }
      """.data(using: .utf8)!
      
      let container = try! JSONContainer.decoding(myJSON)
      
      print(try! container.get( Product.self))
      print(try! container.get(Employee.self))
      

      Product(name: "PR1", price: 20)
      Employee(lastName: "Smith", department: "IT", manager: "Anderson")
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-04-17
        • 1970-01-01
        • 2011-11-30
        • 2017-10-14
        相关资源
        最近更新 更多