Swift Decodable-如何解码已被base64编码的嵌套JSON

问题描述

我正在尝试从第三方API解码JSON响应,该API包含已被base64编码的嵌套/子JSON。

设计示例JSON

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

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

我目前有一些代码可以对此进行解码,但是很遗憾,我必须在JSONDecoder()内重新实例化一个init,这样做并不酷...

设计示例代码


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:我无法控制响应格式,因此无法更改。

解决方法

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

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

如果无法解码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 Decoder一样,它是基本的,而且只是为了好玩(即:不要复制它)在家^^):

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,}
"""

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"))
,

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

您可以创建一个新的NestedJSONDecodable协议,该协议还将JSONDecoder放入其初始化程序中:

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

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

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)
    }
}

其余代码将保持不变。

,

您可以将单个解码器创建为static的{​​{1}}属性,对其进行一次配置,然后将其用于内部和内部的所有Model解码需求。

主动提出的想法: 老实说,我只建议这样做,如果您发现由于分配额外的JSONDecoder而导致CPU时间明显减少或疯狂堆增长……它们不是重量级的对象,少于128个字节,除非有一些我不明白的技巧(虽然在tbh中很常见):

Model