I am attempting to decode a JSON response from a third-party API which contains nested/child JSON that has been base64 encoded.
Contrived Example JSON
{
"id": 1234,
"attributes": "eyAibmFtZSI6ICJzb21lLXZhbHVlIiB9",
}
PS "eyAibmFtZSI6ICJzb21lLXZhbHVlIiB9"
is { 'name': 'some-value' }
base64 encoded.
I have some code that is able to decode this at present but unfortunately I have to reinstanciate an additional JSONDecoder()
inside of the init
in order to do so, and this is not cool...
Contrived Example Code
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)
}
}
Is there anyway to achieve the decoding without instanciating the additional JSONDecoder
?
PS: I have no control over the response format and it cannot be changed.
I find the question interesting, so here is a possible solution which would be to give the main decoder an additional one in its 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]
Because the main method we use from JSONDecoder()
is func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable
and I wanted to keep it as such, I created a protocol:
protocol BasicDecoder {
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable
}
extension JSONDecoder: BasicDecoder {}
And I made JSONDecoder
respects it (and since it already does...)
Now, to play a little and check what could be done, I created a custom one, in the idea of having like you said a XML Decoder, it's basic, and it's just for the fun (ie: do no replicate this at home ^^):
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
}
}
So, 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)
Let's try it now!
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)")
}
Output:
$> 1: Model(id: 1234, attributes: Quick.Attributes(name: "some-value"))
$> 2: Model(id: 1234, attributes: Quick.Attributes(name: "some-value"))