在 Swift 中动态解码任意 json 字段
Decoding arbitrary json field dynamically in Swift
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 的 "subtree" 然后将其传递给解码器?在 swift.
中似乎不允许在泛型函数中定义结构
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")
我首先要说的是 是一个可行且好的答案,但如果您寻求一种不同的方式来做这件事,尽管表面下的工作方式基本相同,我有一个替代解决方案,使用 Code Different 答案的主要组成部分,生成以下代码。主要区别之一是一个 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")
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 的 "subtree" 然后将其传递给解码器?在 swift.
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")
我首先要说的是 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")