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