Swift 可解码 - 如何解码已进行 base64 编码的嵌套 JSON

Swift Decodable - How to decode nested JSON that has been base64 encoded

我正在尝试解码来自第三方 API 的 JSON 响应,其中包含经过 base64 编码的 nested/child JSON。

人为的例子JSON

{
   "id": 1234,
   "attributes": "eyAibmFtZSI6ICJzb21lLXZhbHVlIiB9",  
}

PS "eyAibmFtZSI6ICJzb21lLXZhbHVlIiB9"{ 'name': 'some-value' } base64 编码。

我目前有一些代码可以对此进行解码,但不幸的是,我必须在 init 中重新实例化一个额外的 JSONDecoder() 才能这样做,这并不酷。 ..

人为的示例代码


struct Attributes: Decodable {
    let name: String
}

struct Model: Decodable {

    let id: Int64
    let attributes: Attributes

    private enum CodingKeys: String, CodingKey {
        case id
        case attributes
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.id = try container.decode(Int64.self, forKey: .id)

        let encodedAttributesString = try container.decode(String.self, forKey: .attributes)

        guard let attributesData = Data(base64Encoded: encodedAttributesString) else {
            fatalError()
        }

        // HERE IS WHERE I NEED HELP
        self.attributes = try JSONDecoder().decode(Attributes.self, from: attributesData)
    }
}

有没有办法在不实例化额外的JSONDecoder的情况下实现解码?

PS: 我无法控制响应格式,无法更改。

您可以创建一个解码器作为 Modelstatic 属性,配置一次,然后将其用于所有 Model 解码需求,无论是外部的和内部。

不请自来的想法: 老实说,如果您看到可测量的 CPU 时间损失或分配额外的 JSONDecoder 导致堆疯狂增长,我只会建议您这样做……它们不是重量级对象,小于 128 字节,除非我有一些诡计不明白(虽然说实话这很常见):

let decoder = JSONDecoder()
malloc_size(Unmanaged.passRetained(decoder).toOpaque()) // 128

阅读 this interesting post 后,我想到了一个可重复使用的解决方案。

您可以创建一个新的 NestedJSONDecodable 协议,它的初始化程序中也包含 JSONDecoder

protocol NestedJSONDecodable: Decodable {
    init(from decoder: Decoder, using nestedDecoder: JSONDecoder) throws
}

实施解码器提取技术(来自上述 post)以及用于解码 NestedJSONDecodable 类型的新 decode(_:from:) 函数:

protocol DecoderExtractable {
    func decoder(for data: Data) throws -> Decoder
}

extension JSONDecoder: DecoderExtractable {
    struct DecoderExtractor: Decodable {
        let decoder: Decoder
        
        init(from decoder: Decoder) throws {
            self.decoder = decoder
        }
    }
    
    func decoder(for data: Data) throws -> Decoder {
        return try decode(DecoderExtractor.self, from: data).decoder
    }
    
    func decode<T: NestedJSONDecodable>(_ type: T.Type, from data: Data) throws -> T {
        return try T(from: try decoder(for: data), using: self)
    }
}

并更改您的 Model 结构以符合 NestedJSONDecodable 协议而不是 Decodable:

struct Model: NestedJSONDecodable {

    let id: Int64
    let attributes: Attributes

    private enum CodingKeys: String, CodingKey {
        case id
        case attributes
    }

    init(from decoder: Decoder, using nestedDecoder: JSONDecoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.id = try container.decode(Int64.self, forKey: .id)
        let attributesData = try container.decode(Data.self, forKey: .attributes)
        
        self.attributes = try nestedDecoder.decode(Attributes.self, from: attributesData)
    }
}

其余代码将保持不变。

如果 attributes 只包含一个键值对,这是简单的解决方案。

它将base64编码的字符串直接解码为Data——这可以通过.base64数据解码策略实现——并使用传统的JSONSerialization反序列化。该值分配给 Model 结构中的成员 name

如果无法解码base64编码的字符串,将抛出DecodingError

let jsonString = """
{
   "id": 1234,
   "attributes": "eyAibmFtZSI6ICJzb21lLXZhbHVlIiB9",
}
"""

struct Model: Decodable {
    
    let id: Int64
    let name: String
    
    private enum CodingKeys: String, CodingKey {
        case id, attributes
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(Int64.self, forKey: .id)
        let attributeData = try container.decode(Data.self, forKey: .attributes)
        guard let attributes = try JSONSerialization.jsonObject(with: attributeData) as? [String:String],
            let attributeName = attributes["name"] else { throw DecodingError.dataCorruptedError(forKey: .attributes, in: container, debugDescription: "Attributes isn't eiter a dicionary or has no key name") }
        self.name = attributeName
    }
}

let data = Data(jsonString.utf8)

do {
    let decoder = JSONDecoder()
    decoder.dataDecodingStrategy = .base64
    let result = try decoder.decode(Model.self, from: data)
    print(result)
} catch {
    print(error)
}

我觉得这个问题很有趣,所以这里有一个可能的解决方案,就是在 userInfo:

中给主解码器一个额外的解码器
extension CodingUserInfoKey {
    static let additionalDecoder = CodingUserInfoKey(rawValue: "AdditionalDecoder")!
}

var decoder = JSONDecoder()
let additionalDecoder = JSONDecoder() //here you can put the same one, you can add different options, same ones, etc.
decoder.userInfo = [CodingUserInfoKey.additionalDecoder: additionalDecoder]

因为我们在 JSONDecoder() 中使用的主要方法是 func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable 并且我想保持原样,所以我创建了一个协议:

protocol BasicDecoder {
    func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable
}

extension JSONDecoder: BasicDecoder {}

我让 JSONDecoder 尊重它(因为它已经这样做了......)

现在,为了玩一下并检查可以做什么,我创建了一个自定义的,就像你说的那样 XML 解码器,它是基本的,只是为了好玩(即:不要在家里复制这个 ^^):

struct CustomWithJSONSerialization: BasicDecoder {
    func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable {
        guard let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { fatalError() }
        return Attributes(name: dict["name"] as! String) as! T
    }
}

所以,init(from:):

guard let attributesData = Data(base64Encoded: encodedAttributesString) else { fatalError() }
guard let additionalDecoder = decoder.userInfo[.additionalDecoder] as? BasicDecoder else { fatalError() }
self.attributes = try additionalDecoder.decode(Attributes.self, from: attributesData)

现在就来试试吧!

var decoder = JSONDecoder()
let additionalDecoder = JSONDecoder()
decoder.userInfo = [CodingUserInfoKey.additionalDecoder: additionalDecoder]


var decoder2 = JSONDecoder()
let additionalDecoder2 = CustomWithJSONSerialization()
decoder2.userInfo = [CodingUserInfoKey.additionalDecoder: additionalDecoder]


let jsonStr = """
{
"id": 1234,
"attributes": "eyAibmFtZSI6ICJzb21lLXZhbHVlIiB9",
}
"""

let jsonData = jsonStr.data(using: .utf8)!

do {
    let value = try decoder.decode(Model.self, from: jsonData)
    print("1: \(value)")
    let value2 = try decoder2.decode(Model.self, from: jsonData)
    print("2: \(value2)")
}
catch {
    print("Error: \(error)")
}

输出:

$> 1: Model(id: 1234, attributes: Quick.Attributes(name: "some-value"))
$> 2: Model(id: 1234, attributes: Quick.Attributes(name: "some-value"))