Search code examples
swiftcodabledecodable

Swift Decodable - How to decode nested JSON that has been base64 encoded


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.


Solution

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