【问题标题】:Is it possible to decode additional parameters using JSONDecoder?是否可以使用 JSONDecoder 解码其他参数?
【发布时间】:2018-10-11 12:23:03
【问题描述】:

我们有一些后端返回的响应:

{
    "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属性?

【问题讨论】:

  • value#的类型有限制吗?它可以是任何 JSON 值(即它们可以是对象或数组还是 null),或者它们都是“字符串或整数”还是都相同(例如字符串)? (在所有情况下,这都是一个完全可以解决的问题;您可以限制值的类型越多,它就越简单。params 不可能真正成为 [String: Any],因为 JSON 无法编码 Any。所以它会很好将该属性的类型更改为更受限制的类型。)

标签: json swift parsing jsondecoder


【解决方案1】:

Decodable 非常强大。它可以完全解码任意 JSON,所以这只是该问题的一个子集。如需完整的 JSON Decodable,请参阅此 JSON

我将从示例中提取Key 的概念,但为简单起见,我假设值必须是IntString。您可以将 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])

其他想法

我认为下面的 cmets 非常重要,未来的读者应该仔细阅读它们。我想展示需要多少代码重复,以及其中多少可以轻松提取和重用,这样就不需要魔法或动态特性。

首先,提取常用且可重复使用的部分:

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(基本上是你必须为@987654335编写的代码@)。作为交换,我们获得了完整的编译时类型检查,因此我们知道当我们遇到意外情况时它不会崩溃。

有几种方法可以使上述内容更短一些(我目前正在考虑涉及 KeyPaths 的想法,以使其更加方便)。关键是当前的工具非常强大,值得探索。

【讨论】:

  • 谢谢!不幸的是,JSONDecoder 不支持通用解决方案(当我们有其他必需的属性而不是名称和数字时)。也许将来会实施。不过你的回答很有帮助!
  • @AlexBishop 我不确定你所说的“通用”是什么意思。您可以添加所需的任何属性。如果您要解决某个特定问题,有一些方法可以使这个问题变得更加通用(不过,它越通用,就越复杂)。
  • “通用”是指为字典指定主要属性和属性的可能性。可以通过在 JSONDecoder 中指定 userInfo 来完成
  • 我不认为 JSONDecoder 上的用户信息真的是正确的方法。那将不允许类型安全。您是否建议您可以提交Model.self 以及密钥字典?如果键与属性不匹配,您期望会发生什么?我怀疑你想问一个与你问的问题不同的问题。
  • (特别是,解释你想要“在未来实现”的内容会很有帮助。我怀疑 Decodable 已经可以做你想做的事,除非你想要的不是类型安全的,在这种情况下 Swift 可能永远不会允许它。)
猜你喜欢
  • 1970-01-01
  • 2015-07-06
  • 2012-12-26
  • 2014-05-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-02-06
  • 1970-01-01
相关资源
最近更新 更多