首页 文章

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

提问于
浏览
4

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 = getValue<Product>(from: myJson, field: "product")
let employee = getValue<Employee>(from: myJson, field: "employee")

这可能是 JSONDecoder 或者我应该使用 JSONSerialization ,首先读出给定json的"subtree"然后将其传递给解码器?在swift中似乎不允许在泛型函数中定义结构 .

2 回答

  • 3

    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")
    
  • 0

    我建议使用Code Different's answer,但是如果你寻求一种不同的方式,那么在表面下以相同的方式工作,我已经扩展了他们的解决方案,导致下面的代码 .

    我也会推荐这些:


    /// Conforming to this protocol, makes the type decodable using the JSONContainer class
    protocol ContainerCodable: Codable {
        static var containerIdentifier: String { get }
    }
    
    extension ContainerCodable {
        static var containerIdentifier: String {
            return String(describing: self).lowercased()
        }
    }
    
    struct Product: ContainerCodable {
    
        var name:  String
        var price: Int
    }
    
    struct Employee: ContainerCodable {
    
        var lastName:   String
        var department: String
        var manager:    String
    }
    
    class JSONContainer: 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) {
                self.init(stringValue: string)!
            }
        }
    
        private let decoder: JSONDecoder
        private let container: KeyedDecodingContainer<AnyCodingKeys>
    
        required init(from decoder: Decoder) throws {
            self.decoder = JSONDecoder()
            self.container = try decoder.container(keyedBy: AnyCodingKeys.self)
        }
    
        static func decoding(_ data: Data) throws -> JSONContainer {
            let decoder = JSONDecoder()
            return try decoder.decode(JSONContainer.self, from: myJSON)
        }
    
        func get<T: ContainerCodable>(_ type: T.Type) throws -> T {
            return try container.decode(T.self, forKey: AnyCodingKeys(T.containerIdentifier))
        }
    
        func get<T: ContainerCodable>() throws -> T {
            return try get(T.self)
        }
    }
    

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

相关问题