0

I'm receiving the same json structure from two endpoints, the only thing different are the keys in the json. On response #1 I get

[
    {
        "id": 45,
        "chapter__book__name": "Alonso",
        "chapter__book__id": 70,
        "chapter__chapter": 2,
        "verse": "",
        "verse_number": 5,
        "chapter": 97
    },
]

And on response #2 I get:

[
    {
        "id": 962,
        "book_name": "Title here",
        "book_id": 70,
        "chapter_number": 32,
        "verse": "xxx",
        "verse_number": 24,
        "chapter": 127
    },
]

Can one struct decode both of these? Currently my struct looks like this:

struct Verse: Decodable, Identifiable {
    let id: Int
    let book_name: String
    let book_id: Int
    let verse: String
    let verse_number: Int
    let chapter: Int // chapter Id in database
    let chapter_number: Int
}

Which matches response #2, but not response #1.

Asperi
  • 228,894
  • 20
  • 464
  • 690
erikvm
  • 858
  • 10
  • 30
  • Probably one `protocol` but 2 `struts. You need 2 sets of coding keys that merge the types – lorem ipsum Sep 30 '21 at 20:01
  • 2
    What about using 2 structures, each one for each endpoint, and have a third one which is "how your app manage them"? Else, you can use a custom `init(from: decoder)`, but that's some work. – Larme Sep 30 '21 at 20:40

1 Answers1

1

@lorem ipsum's method should work I didn't try it myself with swiftUI, however it feels a bit convoluted to deal with 2 different types of object. Eventhough they share a common protocol, since it's the same object that will be decoded, it seems natural to keep track of one single type.

As stated by @Larme it can be done with a custom init(from decoder: Decoder) method.

import UIKit

let jsonA = """
[
    {
        "id": 45,
        "chapter__book__name": "Alonso",
        "chapter__book__id": 70,
        "chapter__chapter": 2,
        "verse": "",
        "verse_number": 5,
        "chapter": 97
    },
]
"""

let jsonB = """
[
    {
        "id": 962,
        "book_name": "Title here",
        "book_id": 70,
        "chapter_number": 32,
        "verse": "xxx",
        "verse_number": 24,
        "chapter": 127
    },
]
"""

protocol VerseCodingKey: CodingKey {
    static var id: Self { get }
    static var book_name: Self { get }
    static var book_id: Self { get }
    static var verse: Self { get }
    static var verse_number: Self { get }
    static var chapter: Self { get }
    static var chapter_number: Self { get }
}

struct Verse: Decodable {
    var id: Int
    var book_name: String
    var book_id: Int
    var verse: String
    var verse_number: Int
    var chapter: Int
    var chapter_number: Int
    
    enum CodingKeysA: String, VerseCodingKey {
        case id
        case book_name
        case book_id
        case verse
        case verse_number
        case chapter
        case chapter_number
    }
    
    enum CodingKeysB: String, VerseCodingKey {
        case id
        case book_name = "chapter__book__name"
        case book_id = "chapter__book__id"
        case verse
        case verse_number
        case chapter = "chapter__chapter"
        case chapter_number = "chapter"
    }
    
    init(from decoder: Decoder) throws {
        do {
            try self.init(from: decoder, verseCodingKey: CodingKeysA.self)
            return
        } catch { }
        
        do {
            try self.init(from: decoder, verseCodingKey: CodingKeysB.self)
            return
        } catch { }
        
        throw CustomError.unmatchedCodingKeys
    }
    
    init<T: VerseCodingKey>(from decoder: Decoder, verseCodingKey: T.Type) throws {
        do {
            let values = try decoder.container(keyedBy: T.self)
            id = try values.decode(Int.self, forKey: .id)
            book_name = try values.decode(String.self, forKey: .book_name)
            book_id = try values.decode(Int.self, forKey: .book_id)
            verse = try values.decode(String.self, forKey: .verse)
            verse_number = try values.decode(Int.self, forKey: .verse_number)
            chapter = try values.decode(Int.self, forKey: .chapter)
            chapter_number = try values.decode(Int.self, forKey: .chapter_number)
        } catch {
            throw CustomError.missingCodingKey
        }
    }
}

enum CustomError: Error {
    case missingCodingKey
    case unmatchedCodingKeys
 }

let dataA = jsonA.data(using: .utf8)!
let dataB = jsonB.data(using: .utf8)!
let verseA = try? JSONDecoder().decode([Verse].self, from: dataA)
let verseB = try? JSONDecoder().decode([Verse].self, from: dataB)

This code works on playground


SideNotes:

The whole point is to juggle with two different CodingKeys.

since this evolution it is now feasible to make an enum conform to protocols, which I didn't now of before diving into your issue. This makes the code more straightforward and reusable.

There may be a better way to handle the do catch mechanism but it's acceptable at this point. as stated by @Cristik in comment, you should enhance the error handling mechanism because you don't want to let all the error going through. see his comment below

This is how far I could get with this little experiment, I reckon someone will be able to do better. It still seem more reliable to use a single concrete class instead of two plus a protocol, but again, I'm not pretending to be an expert.

Olympiloutre
  • 2,268
  • 3
  • 28
  • 38
  • @Cristik I didn't get you for the wrap part I must say. The thing is that if the `decode` method throws, it basically means that the wrong codingKeys have been used to try decoding the json, therefore when it fails, the method has to be called with another set of Keys. Without the `return`s statements, it would always throw at the end. I want the method to return when the decoding is successful, and throw if we reach the end of init's scope – Olympiloutre Oct 01 '21 at 10:19
  • @Cristik I think we are misunderstanding. I mean, in this `init(from decoder: Decoder)` if I got rid of the `return` statement, it would always reach the `throw CustomError.unmatchedCodingKeys` which is not wanted. I want it to end when the `init(from decoder:, verseCodingKey:)` doesnt throw, which meant that the decoding is successful. I know how `do catch` works I assure you haha – Olympiloutre Oct 01 '21 at 10:44
  • @Cristik totally agree on what you said concerning the `wrap` part that you mensionned in the second comment. This is a proof of concept, not something that is meant to be copy/pasted. The strategy is here, now it has to be adapted and enhanced in accordance with his needs and the project structure (along with some good practices such as not bypassing all errors that can be thrown) – Olympiloutre Oct 01 '21 at 10:47
  • @Cristik nice catch ! it's a leftover I got rid of it! thanks – Olympiloutre Oct 01 '21 at 11:32