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
有什么缺点? (我认为你无法避免)
我能想到的一些原因...因为新的解码器可能有与原始解码器不同的选项(例如convertFromSnakeCase
或dateDecodingStrategy
),因为数据格式可能根本不是 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】:
您可以将单个解码器创建为Model
的static
属性,配置一次,然后将其用于您的所有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 解码类型更改的最简单方法