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

6

我正尝试解码来自第三方API的JSON响应,其中包含了被base64编码的嵌套/子JSON数据。

虚构的JSON示例

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

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

目前我有一些能够解码它的代码,但不幸的是我必须在 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 的情况下完成解码?

附注:我无法控制响应格式,它不能被更改。


好奇问一下,使用额外的 JSONDecoder 有什么缺点?(我认为你无法避免它) - New Dev
我能想到的一些原因是... 因为新的解码器可能会有不同的选项,例如 convertFromSnakeCasedateDecodingStrategy,因为数据格式可能根本不是 JSON,有人可能正在尝试以 XML 格式解码相同的模型。 - Oliver Pearmain
1
您可以将自定义解码器(可以是具有相同选项的解码器)放置在“主”解码器的“userInfo”中。 - Larme
@Larme所说的,以及它可能是父对象解码器中不同的数据格式(例如JSON内部的XML)是我认为它应该是一个额外的(或不同的)解码器的原因。 - New Dev
4个回答

3
如果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中的示例是人为制造和简化的。 - Oliver Pearmain
建议的解决方案只是将JSONDecoder替换为JSONSerialization,这并不能真正解决代码异味的问题。 - Oliver Pearmain
你无法避免气味,因为必须解码两个不同的级别。 - vadian

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

我已经实现了这种方法。感谢您提供的出色答案和写作,我已经接受了! - Oliver Pearmain

1

阅读了这篇有趣的文章后,我想到了一个可重复使用的解决方案。

您可以创建一个新的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)
    }
}

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

谢谢,这绝对是个好答案(我已点赞),但我看到一个缺点,即不可能将Model作为另一个模型的子/嵌套/子实体。 - Oliver Pearmain
@OliverPearmain,没错,我也想过这个。说实话,我更喜欢Larme的回答 :) - gcharita

0

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

不请自来的想法: 老实说,我只会建议在从分配额外的JSONDecoders中看到可衡量的CPU时间损失或疯狂的堆增长时才这样做...它们不是重量级对象,除非有些我不理解的诡计(但这很常见),少于128字节:

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

谢谢您的建议,但实际上这只是把一个问题换成了另一个问题。我已经考虑过使用单例模式了。 - Oliver Pearmain

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