Можно ли декодировать дополнительные параметры с помощью JSONDecoder?

У нас есть некоторый ответ, возвращенный бэкэндом:

{
    "name": "Some name",
    "number": 42,
    ............
    "param0": value0,
    "param1": value1,
    "param2": value2
}

Структура модели для ответа:

struct Model: Codable {
    let name: String
    let number: Int
    let params: [String: Any]
}

Как заставить JSONDecoder объединить все неизвестные пары ключ-значение в свойство params?


person Alex Bishop    schedule 01.05.2018    source источник
comment
Существуют ли ограничения на типы value#? Может ли это быть любое значение JSON (т. е. могут ли они быть объектом, массивом или нулевым значением), или все они являются строками или целыми числами, или все они одинаковы (например, строки)? (Это полностью решаемая проблема во всех случаях; она тем проще, чем больше вы можете ограничить типы значений. Невозможно, чтобы params действительно было [String: Any], поскольку JSON не может кодировать Any. Поэтому было бы неплохо изменить тип этого свойства на что-то более ограниченное.)   -  person Rob Napier    schedule 01.05.2018


Ответы (1)


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

Я возьму концепцию Key из примера, но для простоты предположу, что значения должны быть либо Int, либо String. Вы можете сделать parameters [String: JSON] и вместо этого использовать мой декодер JSON.

struct Model: Decodable {
    let name: String
    let number: Int
    let params: [String: Any]

    // An arbitrary-string Key, with a few "well known and required" keys
    struct Key: CodingKey, Equatable {
        static let name = Key("name")
        static let number = Key("number")

        static let knownKeys = [Key.name, .number]

        static func ==(lhs: Key, rhs: Key) -> Bool {
            return lhs.stringValue == rhs.stringValue
        }

        let stringValue: String
        init(_ string: String) { self.stringValue = string }
        init?(stringValue: String) { self.init(stringValue) }
        var intValue: Int? { return nil }
        init?(intValue: Int) { return nil }
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: Key.self)

        // First decode what we know
        name = try container.decode(String.self, forKey: .name)
        number = try container.decode(Int.self, forKey:. number)

        // Find all the "other" keys
        let optionalKeys = container.allKeys
            .filter { !Key.knownKeys.contains($0) }

        // Walk through the keys and try to decode them in every legal way
        // Throw an error if none of the decodes work. For this simple example
        // I'm assuming it is a String or Int, but this is also solvable for
        // arbitarily complex data (it's just more complicated)
        // This code is uglier than it should be because of the `Any` result.
        // It could be a lot nicer if parameters were a more restricted type
        var p: [String: Any] = [:]
        for key in optionalKeys {
            if let stringValue = try? container.decode(String.self, forKey: key) {
                p[key.stringValue] = stringValue
            } else {
                 p[key.stringValue] = try container.decode(Int.self, forKey: key)
            }
        }
        params = p
    }
}

let json = Data("""
{
    "name": "Some name",
    "number": 42,
    "param0": 1,
    "param1": "2",
    "param2": 3
}
""".utf8)

try JSONDecoder().decode(Model.self, from: json)
// Model(name: "Some name", number: 42, params: ["param0": 1, "param1": "2", "param2": 3])

ДОПОЛНИТЕЛЬНЫЕ МЫСЛИ

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

Во-первых, извлеките части, которые являются общими и повторно используемыми:

func additionalParameters<Key>(from container: KeyedDecodingContainer<Key>,
                               excludingKeys: [Key]) throws -> [String: Any]
    where Key: CodingKey {
        // Find all the "other" keys and convert them to Keys
        let excludingKeyStrings = excludingKeys.map { $0.stringValue }

        let optionalKeys = container.allKeys
            .filter { !excludingKeyStrings.contains($0.stringValue)}

        var p: [String: Any] = [:]
        for key in optionalKeys {
            if let stringValue = try? container.decode(String.self, forKey: key) {
                p[key.stringValue] = stringValue
            } else {
                p[key.stringValue] = try container.decode(Int.self, forKey: key)
            }
        }
        return p
}

struct StringKey: CodingKey {
    let stringValue: String
    init(_ string: String) { self.stringValue = string }
    init?(stringValue: String) { self.init(stringValue) }
    var intValue: Int? { return nil }
    init?(intValue: Int) { return nil }
}

Теперь декодер для Model сводится к этому

struct Model: Decodable {
    let name: String
    let number: Int
    let params: [String: Any]

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: StringKey.self)

        name = try container.decode(String.self, forKey: StringKey("name"))
        number = try container.decode(Int.self, forKey: StringKey("number"))
        params = try additionalParameters(from: container,
                                          excludingKeys: ["name", "number"].map(StringKey.init))
    }
}

Было бы неплохо, если бы был какой-то волшебный способ сказать «пожалуйста, позаботьтесь об этих свойствах по умолчанию», но я, честно говоря, не совсем понимаю, как это будет выглядеть. Объем кода здесь примерно такой же, как для реализации NSCoding, и намного меньше, чем для реализации против NSJSONSerialization, и его легко передать в swiftgen, если это слишком утомительно (в основном это код, который вы должны написать для init). Взамен мы получаем полную проверку типов во время компиляции, поэтому мы знаем, что она не рухнет, когда мы получим что-то неожиданное.

Есть несколько способов сделать даже вышеизложенное немного короче (и в настоящее время я обдумываю идеи, связанные с KeyPath, чтобы сделать его еще более удобным). Дело в том, что текущие инструменты очень мощные и заслуживают изучения.

person Rob Napier    schedule 01.05.2018
comment
Благодарю вас! К сожалению, JSONDecoder не поддерживает универсальное решение (когда у нас есть другие необходимые свойства вместо имени и номера). Возможно, это будет реализовано в будущем. Но ваш ответ очень полезен! - person Alex Bishop; 01.05.2018
comment
@AlexBishop Я не уверен, что вы имеете в виду под универсальным. Вы можете добавить любые необходимые свойства. Есть способы сделать это гораздо более общим, если у вас есть конкретная проблема, которую вы пытаетесь решить (однако, чем она более общая, тем сложнее она становится). - person Rob Napier; 01.05.2018
comment
Под универсальным я подразумеваю возможность указать основные свойства и свойства для словаря. Это можно сделать, указав userInfo в JSONDecoder. - person Alex Bishop; 01.05.2018
comment
Я не думаю, что информация о пользователе в JSONDecoder действительно правильный подход. Это не позволит обеспечить безопасность типов. Вы предлагаете передать Model.self, а также словарь ключей? Что бы вы ожидали, если бы ключи не соответствовали свойствам? Я подозреваю, что вы хотите задать вопрос, отличный от того, который вы задали. - person Rob Napier; 01.05.2018
comment
(В частности, было бы полезно объяснить, что вы хотели бы реализовать в будущем. Я подозреваю, что Decodable уже может делать то, что вы хотите, если только то, что вы хотите, не является безопасным с точки зрения типов, и в этом случае Swift, вероятно, никогда этого не допустит. .) - person Rob Napier; 01.05.2018
comment
Я имел в виду, что может быть много моделей для разных ответов. Model1: {имя, номер, параметры}, Model2: {id, дата, параметры} и так далее. И главная проблема заключается в том, чтобы избежать дублирования кода. - person Alex Bishop; 01.05.2018
comment
Почти все это можно извлечь во вспомогательный объект, чтобы избежать дублирования большей части кода, кроме той части, которая меняется. Жаль, что Swift не может унаследовать большую часть своей реализации Codable по умолчанию для пересылки, но если бы у вас было значительное их количество, SwiftGen быстро с этим справился бы. Чрезмерно умные решения представляют собой гораздо большую угрозу для проектов, чем скромное дублирование кода, когда потребности на самом деле разные (а те части, которые не отличаются, сегодня могут быть исключены). - person Rob Napier; 01.05.2018