Decodable
невероятно мощный. Он может декодировать совершенно произвольный JSON, так что это лишь часть этой проблемы. Полностью отработанный JSON Decodable
см. В этом JSON .
Я приведу концепцию Key
из примера, но для простоты предположу, что значения должны быть либо Int
, либо String
. Вы можете сделать parameters
be [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, если он слишком утомителен (это в основном код, который вы должны написать для * 1029) *). Взамен мы получаем полную проверку типов во время компиляции, поэтому мы знаем, что она не вылетит, когда мы получим что-то неожиданное.
Есть несколько способов сделать даже вышеупомянутое немного короче (и в настоящее время я думаю об идеях, связанных с KeyPaths, чтобы сделать его еще более удобным). Дело в том, что современные инструменты очень мощные и заслуживают изучения.