Swift 4动态JSON解码

3
我将尝试在Swift 4中解码以下JSON:
{
    "token":"RdJY3RuB4BuFdq8pL36w",
    "permission":"accounts, users",
    "timout_in":600,
    "issuer": "Some Corp",
    "display_name":"John Doe",
    "device_id":"uuid824fd3c3-0f69-4ee1-979a-e8ab25558421"
}

问题在于 JSON 中的最后两个元素(display_namedevice_id)可能存在也可能不存在,或者这些元素可能以完全不同但仍未知的名称命名,例如"fred": "worker", "hours" : 8 所以我想要做到的是解码已知的内容,例如tokenpermissiontimeout_inissuer 以及任何其他元素(如display_namedevice_id等),并将它们放入字典中。
我的结构看起来像这样:
struct AccessInfo : Decodable
{
    let token: String
    let permission: [String]
    let timeout: Int
    let issuer: String
    let additionalData: [String: Any]

    private enum CodingKeys: String, CodingKey
    {
        case token
        case permission
        case timeout = "timeout_in"
        case issuer
    }

    public init(from decoder: Decoder) throws
    {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        token = container.decode(String.self, forKey: .token)
        permission = try container.decodeIfPresent(String.self, forKey: .permission).components(separatedBy: ",")
        timeout = try container.decode(Int.self, forKey: . timeout)
        issuer = container.decode(String.self, forKey: .issuer)

        // This is where I'm stuck, how do I add the remaining
        // unknown JSON elements into additionalData?
    }
}

// Calling code, breviated for clarity
let decoder = JSONDecoder()
let accessInfo = try decoder.decode(AccessInfo.self, from: data!)

如果有人能提供一些指导,那么我现在的问题是能够解码已知结构的某些部分,其中JSON可能包含动态信息。

谢谢


您的 JSON 不是有效的。请提供 真正的 JSON。 - matt
Swift 4中使用在解码时才知道的键的可解码协议。 - matt
2个回答

8

受@matt评论的启发,这是我采用的完整样例。我扩展了KeyedDecodingContainer来解码未知的键,并提供了一个参数来过滤已知的CodingKeys

示例JSON

{
    "token":"RdJY3RuB4BuFdq8pL36w",
    "permission":"accounts, users",
    "timout_in":600,
    "issuer": "Some Corp",
    "display_name":"John Doe",
    "device_id":"uuid824fd3c3-0f69-4ee1-979a-e8ab25558421"
}

Swift结构体

struct AccessInfo : Decodable
{
    let token: String
    let permission: [String]
    let timeout: Int
    let issuer: String
    let additionalData: [String: Any]

    private enum CodingKeys: String, CodingKey
    {
        case token
        case permission
        case timeout = "timeout_in"
        case issuer
    }

    public init(from decoder: Decoder) throws
    {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        token = container.decode(String.self, forKey: .token)
        permission = try container.decode(String.self, forKey: .permission).components(separatedBy: ",")
        timeout = try container.decode(Int.self, forKey: . timeout)
        issuer = container.decode(String.self, forKey: .issuer)

        // Additional data decoding
        let container2 = try decoder.container(keyedBy: AdditionalDataCodingKeys.self)
        self.additionalData = container2. decodeUnknownKeyValues(exclude: CodingKeys.self)
    }
}

private struct AdditionalDataCodingKeys: CodingKey
{
    var stringValue: String
    init?(stringValue: String)
    {
        self.stringValue = stringValue
    }

    var intValue: Int?
    init?(intValue: Int)
    {
        return nil
    }
}

扩展KeyedDecodingContainer

extension KeyedDecodingContainer where Key == AdditionalDataCodingKeys
{
    func decodeUnknownKeyValues<T: CodingKey>(exclude keyedBy: T.Type) -> [String: Any]
    {
        var data = [String: Any]()

        for key in allKeys
        {
            if keyedBy.init(stringValue: key.stringValue) == nil
            {
                if let value = try? decode(String.self, forKey: key)
                {
                    data[key.stringValue] = value
                }
                else if let value = try? decode(Bool.self, forKey: key)
                {
                    data[key.stringValue] = value
                }
                else if let value = try? decode(Int.self, forKey: key)
                {
                    data[key.stringValue] = value
                }
                else if let value = try? decode(Double.self, forKey: key)
                {
                    data[key.stringValue] = value
                }
                else if let value = try? decode(Float.self, forKey: key)
                {
                    data[key.stringValue] = value
                }
                else
                {
                    NSLog("Key %@ type not supported", key.stringValue)
                }
            }
        }

        return data
    }
}

调用代码

let decoder = JSONDecoder()
let accessInfo = try decoder.decode(AccessInfo.self, from: data!)

print("Token: \(accessInfo.token)")
print("Permission: \(accessInfo.permission)")
print("Timeout: \(accessInfo.timeout)")
print("Issuer: \(accessInfo.issuer)")
print("Additional Data: \(accessInfo.additionalData)")

输出

Token: RdJY3RuB4BuFdq8pL36w
Permission: ["accounts", "users"]
Timeout: 600
Issuer: "Some Corp"
Additional Data: ["display_name":"John Doe", "device_id":"uuid824fd3c3-0f69-4ee1-979a-e8ab25558421"]

经过数小时的搜索,这终于帮了我!我不理解的是以下这行代码:if keyedBy.init(stringValue: key.stringValue) == nil。为什么会返回nil? - Sami

1
这个问题实际上是Swift 4 Decodable with keys not known until decoding time的重复。一旦你了解了关于构建一个基础最小CodingKey采用者结构作为你的编码键的诀窍,你就可以将其用于任何字典中。
在这种情况下,您将使用键控容器的allKeys来获取未知的JSON字典键。
为了演示,我将仅限于JSON字典的完全未知部分。想象一下这个JSON:
let j = """
{
    "display_name":"John Doe",
    "device_id":"uuid824fd3c3-0f69-4ee1-979a-e8ab25558421"
}
"""
let jdata = j.data(using: .utf8)!

假设我们不知道那个字典里有什么内容,除了它的键和值都是字符串。因此,我们想要解析jdata,而不知道它的键是什么。

因此,我们有一个结构体,其中包含一个字典属性:

struct S {
    let stuff : [String:String]
}

现在的问题是如何将JSON解析成该结构体 - 也就是说,如何使该结构体符合Decodable并处理该JSON。
以下是方法:
struct S : Decodable {
    let stuff : [String:String]
    private struct CK : CodingKey {
        var stringValue: String
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        var intValue: Int?
        init?(intValue: Int) {
            return nil
        }
    }
    init(from decoder: Decoder) throws {
        let con = try! decoder.container(keyedBy: CK.self)
        var d = [String:String]()
        for key in con.allKeys {
            let value = try! con.decode(String.self, forKey:key)
            d[key.stringValue] = value
        }
        self.stuff = d
    }
}

现在我们解析:
let s = try! JSONDecoder().decode(S.self, from: jdata)

我们得到一个S实例,它的stuff是这个字典:

["device_id": "uuid824fd3c3-0f69-4ee1-979a-e8ab25558421", "display_name": "John Doe"]

而这正是我们想要的结果。


如果未知数据的类型不总是[String:String],那该怎么办? - Santhosh R
这是一个不同的问题,这个问题在 Stack Overflow 上也已经得到了充分的讨论。 - matt
感谢回复。我已经查看了@matt的代码示例swift-4-decodable-with-keys-not-known-until-decoding-time,它可以正常工作。但是,当在单个解析/解码操作中结合已知键未知键时,实现变得棘手,因为您需要两次解码容器...或者采用通用方法进行解码,并将已知键手动分配到字典[String:String]中,将其余未知键丢弃。 - user9041624
你需要两次进入,一次使用已知的密钥,就像你已经展示过的那样,另一次则深入搜索未知的密钥,就像我所做的那样。我需要把整件事情都讲清楚吗? - matt
有人能提供 Stack Overflow 上关于已知键但未知数据类型的问题链接吗? - Martheli

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接