Динамическое декодирование произвольного поля json в 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, а затем передать его декодеру? Определение структур внутри универсальных функций, по-видимому, не разрешено в Swift.


person frangulyan    schedule 09.12.2018    source источник


Ответы (2)


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")
person Code Different    schedule 09.12.2018

Я бы начал с того, что Code Different answer является жизнеспособным и хорошим ответом, но если вы ищете другой способ сделать это , хотя под поверхностью работает в основном так же, у меня есть альтернативное решение, использующее основные компоненты ответа Code Different, что приводит к коду ниже. Одним из основных отличий является тот факт, что один JSONDecoder повторно используется в одном и том же JSON для каждого struct, который вы извлекаете, используя this.

Я бы еще порекомендовал эти:


/// 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")
person Andreas detests censorship    schedule 10.12.2018