Динамическое декодирование произвольного поля json в Swift - PullRequest
0 голосов
/ 10 декабря 2018

TL; DR

Есть ли способ, которым я могу использовать JSONDecoder и написать функцию, которая будет просто считывать из заданного json заданного значения поля указанного декодируемого типа?


Imaging У меня есть следующий 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, по-видимому, недопустимо.

Ответы [ 2 ]

0 голосов
/ 10 декабря 2018

Я бы начал с того, что Ответ Code Different является жизнеспособным и хорошим ответом, но если вы ищете другой способ сделать это, хотя работать в основном так же под поверхностью, у меня есть альтернативарешение, используя основные компоненты ответа 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")
0 голосов
/ 10 декабря 2018

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