在 Swift 中动态解码任意 json 字段

Posted

技术标签:

【中文标题】在 Swift 中动态解码任意 json 字段【英文标题】:Decoding arbitrary json field dynamically in Swift 【发布时间】: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 中似乎不允许在泛型函数中定义结构。

【问题讨论】:

【参考方案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,使用这个。

我也会推荐这些:

How to use Any in Codable Type

Swift 4 Codable; How to decode object with single root-level key


/// 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")

【讨论】:

以上是关于在 Swift 中动态解码任意 json 字段的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Swift 的 Decodable 解析任意 JSON 字符串,而您只知道或关心几个字段? [关闭]

Swift之Codable自定义解析将任意数据类型解析为想要的类型

Swift之Codable自定义解析将任意数据类型解析为想要的类型

动态 JSON 解码 Swift 4

如何在 Swift 中使用动态键(在根级别)解码此 JSON?

如何在 Swift 4 中为 JSON 编写可解码,其中键是动态的?