Search code examples
jsonswiftcodabledecodablejsondecoder

How should I decode a json object using JSONDecoder if I am unsure of the keys


I have an api response in the following shape -

  {
   "textEntries":{
      "summary":{
         "id":"101e9136-efd9-469e-9848-132023d51fb1",
         "text":"some text",
         "locale":"en_GB"
      },
      "body":{
         "id":"3692b0ec-5b92-4ab1-bc25-7711499901c5",
         "text":"some other text",
         "locale":"en_GB"
      },
      "title":{
         "id":"45595d27-7e06-491e-890b-f50a5af1cdfe",
         "text":"some more text again",
         "locale":"en_GB"
      }
   }
}

I'd like to decode this via JSONDecoder so I can use the properties. The challenge I have is the keys, in this case summary,body and title are generated elsewhere and not always these values, they are always unique, but are based on logic that takes place elsewhere in the product, so another call for a different content article could return leftBody or subTitle etc.

The model for the body of these props is always the same however, I can expect the same fields to exist on any combination of responses.

I will need to be able to access the body of each key in code elsewhere. Another API response will tell me the key I need though.

I am not sure how I can handle this with Decodable as I cannot type the values ahead of time.

I had considered something like modelling the body -

struct ContentArticleTextEntries: Decodable {
    var id: String
    var text: String
    var locale: Locale
}

and storing the values in a struct like -

struct ContentArticle: Decodable {
    var textEntries: [String: ContentArticleTextEntries]


    private enum CodingKeys: String, CodingKey {
        case textEntries
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        self.textEntries = try values.decode(ContentArticleTextEntries.self, forKey: .textEntries)

    }

}

I could them maybe use a subscript elsewhere to access property however I do not know how to decode into this shape as the above would not work.

So I would later access like textEntries["body"] for example.

I also do no know if there is a better way to handle this.

I had considered converting the keys to a 'type' using an enum, but again not knowing the enum cases ahead of time makes this impossible.

I know textEntries this does not change and I know id, text and locale this does not change. It is the keys in between this layer I do not know. I have tried the helpful solution posted by @vadian but cannot seem to make this work in the context of only needing 1 set of keys decoded.


Solution

  • For the proposed solution in this answer the structs are

    struct ContentArticleTextEntries: Decodable {
        let title : String
        let id: String
        let text: String
        let locale: Locale
    
        enum CodingKeys: String, CodingKey {
            case id, text, locale
        }
    
        init(from decoder: Decoder) throws {
            self.title = try decoder.currentTitle()
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.id = try container.decode(String.self, forKey: .id)
            self.text = try container.decode(String.self, forKey: .text)
            let localeIdentifier = try container.decode(String.self, forKey: .locale)
            self.locale = Locale(identifier: localeIdentifier)
        }
    }     
    
    struct ContentArticle: TitleDecodable {
        let title : String
        var elements: [ContentArticleTextEntries]
    }
    
    struct Container: Decodable {
        let containers: [ContentArticle]
        init(from decoder: Decoder) throws {
            self.containers = try decoder.decodeTitledElements(ContentArticle.self)
        }
    }
    

    Then decode Container.self