Более простой способ декодирования JSON из нескольких сервисов в Swift - PullRequest
3 голосов
/ 08 января 2020

Premise

У меня есть struct, который соответствует Decodable, поэтому он может декодировать JSON из различных ответов через init(from:). Для каждого типа ответа JSON, который я ожидаю декодировать, у меня есть enum, который соответствует CodingKey.

Пример

Вот упрощенный пример, который можно опустить в Swift Playground:

import Foundation

// MARK: - Services -

struct Service1 {}
struct Service2 {}

// MARK: - Person Model -

struct Person {
    let name: String
}

extension Person: Decodable {
    enum CodingKeys: String, CodingKey {
        case name = "name"
    }

    enum Service2CodingKeys: String, CodingKey {
        case name = "person_name"
    }

    // And so on through service n...

    init(from decoder: Decoder) throws {
        switch decoder.userInfo[.service] {
        case is Service1.Type:
            let container = try decoder.container(keyedBy: CodingKeys.self)
            name = try container.decode(String.self, forKey: .name)
        case is Service2.Type:
            let container = try decoder.container(keyedBy: Service2CodingKeys.self)
            name = try container.decode(String.self, forKey: .name)
        // And so on through service n...
        default:
            fatalError("Missing implementation for service.")
        }
    }
}

// MARK: - CodingUserInfoKey -

extension CodingUserInfoKey {
    static let service = CodingUserInfoKey(rawValue: "service")!
}

// MARK: - Responses -

// The JSON response from service 1.
let service1JSONResponse = """
[
    {
        "name": "Peter",
    }
]
""".data(using: .utf8)!

// The JSON response from service 2.
let service2JSONResponse = """
[
    {
        "person_name": "Paul",
    }
]
""".data(using: .utf8)!

// And so on through service n... where other services have JSON responses with keys of varied names ("full_name", "personName").

// MARK: - Decoding -

let decoder = JSONDecoder()

decoder.userInfo[.service] = Service1.self
let service1Persons = try decoder.decode([Person].self, from: service1JSONResponse)

decoder.userInfo[.service] = Service2.self
let service2Persons = try decoder.decode([Person].self, from: service2JSONResponse)

Проблема

Проблема, с которой я сталкиваюсь, заключается в том, что у меня есть множество различных сервисов, из которых мне нужно было декодировать ответы, и модель со многими другими свойствами. чем этот упрощенный пример. По мере увеличения количества служб увеличивается и количество дел, необходимых для декодирования этих ответов.

Вопрос

Как можно упростить мою реализацию init(from:), чтобы уменьшить все это дублирование кода?

Попытки

Я пытался сохранить правильный CodingKey.Type для каждой службы и передать его в container(keyedBy:), но я получаю эту ошибку:

Невозможно вызвать ' контейнер "со списком аргументов типа" (keyedBy: CodingKey.Type) ".

init(from decoder: Decoder) throws {
    let codingKeyType: CodingKey.Type

    switch decoder.userInfo[.service] {
    case is Service1.Type: codingKeyType = CodingKeys.self
    case is Service2.Type: codingKeyType = Service2CodingKeys.self
    default: fatalError("Missing implementation for service.")
    }

    let container = try decoder.container(keyedBy: codingKeyType) // ← Error
    name = try container.decode(String.self, forKey: .name)
}

Ответы [ 2 ]

2 голосов
/ 11 января 2020

Вместо того, чтобы пытаться решить эту проблему с помощью CodingKeys и усложняющегося init, я предлагаю составить его по протоколу:

protocol PersonLoader: Decodable {
    var name: String { get }
    // additional properties
}

extension Person {
    init(loader: PersonLoader) {
        self.name = loader.name
        // additional properties, but this is one-time
    }
}

В качестве альтернативы, особенно если Person является простым объектом данных, доступным только для чтения, вы можете просто сделать Person протоколом, а затем избежать этого дополнительного шага копирования.

Затем вы можете определить интерфейсы для каждой службы независимо:

struct Service1Person: PersonLoader {
    let name: String
}

struct Service2Person: PersonLoader {
    let person_name: String

    var name: String { person_name }
}

, а затем сопоставить с персонами, когда вы закончите:

let service2Persons = try decoder.decode([Service2Person].self,
                                         from: service2JSONResponse)
    .map(Person.init)

Если вы пошли с подход, основанный только на протоколе, вместо этого он будет выглядеть так:

protocol Person: Decodable {
    var name: String { get }
    // additional properties
}

struct Service1Person: Person {
    let name: String
}

struct Service2Person: Person {
    var name: String { person_name }
    let person_name: String
}

let service2Personsx = try decoder.decode([Service2Person].self,
                                         from: service2JSONResponse) as [Person]
1 голос
/ 11 января 2020

Без набора пользовательских функций для каждой службы (или для типа службы) в Person init (from :), я думаю, что это будет трудно сделать. Вы не можете передавать пользовательские CodingKey -конформантные перечисления в decoder.container(keyedBy:), потому что они генерируются c по типу этого перечисления.

Один из способов сделать это - использовать стратегию декодирования пользовательских ключей и выполнить сопоставление из словаря или с помощью функции в методе / закрытии пользовательского декодирования ключа.

В приведенном ниже примере я использовал перечисление для представления сервисов. Словарь сопоставления вводится в регистр перечисления, поэтому отражает сопоставление ключа службы / типа службы. Надеемся, что это может послужить полезной дорожной картой для более сложного реального использования.

import Foundation

// MARK: - Custom Key Decoding -

struct MyCodingKey: CodingKey {
    var stringValue: String
    var intValue: Int?

    init?(stringValue: String) {
        self.stringValue = stringValue
        self.intValue = nil
    }

    init?(intValue: Int) {
        self.stringValue = String(intValue)
        self.intValue = intValue
    }
}

// MARK: - Services -

enum Services: String {
    case service1
    case service2
}

extension Services {

    var mapping: [String:String] {
        switch self {
        case .service1: return [:]
        case .service2: return ["person_name": "name"]
        }
    }

    func getPersons(jsonData: Data) throws -> [Person] {
        let decoder = JSONDecoder()

        decoder.keyDecodingStrategy = .custom { (keys: [CodingKey]) -> CodingKey in
            let lastKey = keys.last!
            guard lastKey.intValue == nil else {
                return MyCodingKey(intValue: lastKey.intValue!)!
            }
            guard let stringValue = self.mapping[lastKey.stringValue] else {
                return lastKey
            }

            return MyCodingKey(stringValue: stringValue)!
        }

        let persons = try decoder.decode([Person].self, from: jsonData)
        return persons
    }
}

// MARK: - Person Model -

struct Person: Decodable {
    let name: String
}

// MARK: - Responses -

// The JSON response from service 1.
let service1JSONResponse = """
[ {  "name": "Peter", } ]
""".data(using: .utf8)!

// The JSON response from service 2.
let service2JSONResponse = """
[ { "person_name": "Paul", } ]
""".data(using: .utf8)!

// MARK: - Sample Calls -

print((try? Services.service1.getPersons(jsonData: service1JSONResponse))!)
print((try? Services.service2.getPersons(jsonData: service2JSONResponse))!)

...