是否可以使用JSONDecoder解码其他参数? [英] Is it possible to decode additional parameters using 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
属性中?
How to make JSONDecoder
combine all unknown key-value pairs into params
property?
推荐答案
Decodable
非常强大.它可以解码完全任意的JSON,因此这只是该问题的一个子集.有关完全解决的JSON Decodable
,请参见此 JSON .
Decodable
is incredibly powerful. It can decode completely arbitrary JSON, so this is just a sub-set of that problem. For a fully worked-out JSON Decodable
, see this JSON.
我将从示例中提取Key
的概念,但为简单起见,我将假定值必须为Int
或String
.您可以将parameters
设置为[String: JSON]
,并改用我的JSON解码器.
I'll pull the concept of Key
from example, but for simplicity I'll assume that values must be either Int
or String
. You could make parameters
be [String: JSON]
and use my JSON decoder instead.
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])
其他想法
ADDITIONAL THOUGHTS
我认为以下评论非常重要,未来的读者应该仔细阅读.我想展示需要很少的代码重复,并且可以轻松地提取和重用其中的多少代码,这样就不需要魔术或动态功能.
I think the comments below are really important and future readers should look them over. I wanted to show how little code duplication is required, and how much of this can be easily extracted and reused, such that no magic or dynamic features are required.
首先,提取常见且可重复使用的片段:
First, extract the pieces that are common and reusable:
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(基本上,这是您必须为
It would be nice if there were some magic way to say "please take care of these properties in the default way," but I don't quite know what that would look like frankly. The amount of code here is about the same as for implementing NSCoding
, and much less than for implementing against NSJSONSerialization
, and is easily handed to swiftgen if it were too tedious (it's basically the code you have to write for init
). In exchange, we get full compile-time type checking, so we know it won't crash when we get something unexpected.
有几种方法可以使上述内容更短一些(我目前正在考虑有关KeyPath的想法,以使其更加方便).关键是当前的工具非常强大,值得探索.
There are a few ways to make even the above a bit shorter (and I'm currently thinking about ideas involving KeyPaths to make it even more convenient). The point is that the current tools are very powerful and worth exploring.
这篇关于是否可以使用JSONDecoder解码其他参数?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!