Swift Decodable - 如何解码经过 base64 编码的嵌套 JSON

Posted

技术标签:

【中文标题】Swift Decodable - 如何解码经过 base64 编码的嵌套 JSON【英文标题】:Swift Decodable - How to decode nested JSON that has been base64 encoded 【发布时间】:2021-01-14 12:22:43 【问题描述】:

我正在尝试解码来自第三方 API 的 JSON 响应,其中包含经过 base64 编码的嵌套/子 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:我无法控制响应格式,也无法更改。

【问题讨论】:

出于好奇,使用额外的JSONDecoder 有什么缺点? (我认为你无法避免) 我能想到的一些原因...因为新的解码器可能有与原始解码器不同的选项(例如convertFromSnakeCasedateDecodingStrategy),因为数据格式可能根本不是 JSON ,有人可能会尝试以 XML 格式解码相同的模型。 您可以在“主”解码器的userInfo 中放置一个自定义解码器(可以是具有相同选项的解码器)。 @Larme 所说的,......而且,它可能是与父对象的解码器不同的数据格式(例如 JSON 中的 XML),这是我想为什么它的原因应该是一个额外的(或不同的)解码器 【参考方案1】:

如果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)

【讨论】:

"如果属性只包含一个键值对,这是简单的解决方案。"不幸的是,它没有,OP 中的示例是人为设计和简化的。 建议的解决方案只是将JSONDecoder 替换为JSONSerialization,这并不能真正解决代码异味问题。 你无法避免气味,因为它需要解码两个不同的级别。【参考方案2】:

我觉得这个问题很有趣,所以这是一个可能的解决方案,即在其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"))

【讨论】:

我已经实现了这种方法。感谢您的精彩回答并写下,我接受了! ??【参考方案3】:

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

您可以创建一个新的NestedJSONDecodable 协议,该协议还可以在其初始化程序中获取JSONDecoder

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

实现解码器提取技术(来自上述帖子)以及用于解码 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)
    

您的其余代码将保持不变。

【讨论】:

谢谢你,这绝对是一个很好的答案(我赞成)但是我看到了一个缺点,那就是它不可能将Model 作为另一个模型的子/嵌套/子实体。 @OliverPearmain 是的,这绝对是我想到的。老实说,我更喜欢拉尔姆的回答:)【参考方案4】:

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

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

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

【讨论】:

感谢您的建议,但这确实是在用一个问题代替另一个问题。我已经考虑过使用单例。

以上是关于Swift Decodable - 如何解码经过 base64 编码的嵌套 JSON的主要内容,如果未能解决你的问题,请参考以下文章

使用 Swift 4 的 Decodable 解码 Void

Swift 4 JSON Decodable 解码类型更改的最简单方法

如何使用 Swift 的 Decodable 解析任意 JSON 字符串,而您只知道或关心几个字段? [关闭]

使用协议的 Swift 通用解码器

Swift 5 默认可解码实现,只有一个例外

如何在 Swift 中使用可解码?