4

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.

Teddy K
  • 820
  • 1
  • 6
  • 17
  • can u paste your complete json data – Radhe Yadav Nov 20 '19 at 10:30
  • Sorry, I had missed off some braces, I have updated with the valid json response – Teddy K Nov 20 '19 at 10:32
  • What do you mean by ` match up the values on this response with the values from another response.` – PGDev Nov 20 '19 at 10:33
  • Please ignore that, I may have confused my issue. I simply meant I need to access the property of `textEntries` using a key I can get from elsewhere. for example once I have these values, another response may require me to look up `body` etc – Teddy K Nov 20 '19 at 10:35
  • Related: https://stackoverflow.com/questions/54129682/use-swift-codable-to-decode-json-with-values-as-keys/54131007#54131007, a pretty smart solution. – vadian Nov 20 '19 at 10:43
  • consider using nestedContainer: ```let nestedValues = try decoder.nestedContainer(keyedBy: NestedCodingKeys.self, forKey: .WhatEverYourKeyIs)``` – Jafar Khoshtabiat Nov 20 '19 at 10:45
  • Do you know the top level key? `textEntries`? This is always the same? Also the child elements are the same shape too? So in this case you do not know what the keys represented by `body` and `summary` etc will be? – nodediggity Nov 21 '19 at 08:59
  • Yes, 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. – Teddy K Nov 21 '19 at 09:01

3 Answers3

2

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

vadian
  • 274,689
  • 30
  • 353
  • 361
  • Thank you , that is a massive help, this is producing an array of `[textEntries]` - is it possible for `textEntries` to be a single prop with an array of `ContentArticleTextEntries` as it's value? I really really appreciate your help with this. Any pointers on how to amend this would help me greatly. – Teddy K Nov 21 '19 at 10:04
  • 1
    You should amend `Container` to `let containers: [ContentArticleTextEntries]` instead I believe – nodediggity Nov 21 '19 at 10:06
  • 1
    You get the array of `ContentArticleTextEntries` with `containers.first!.elements` – vadian Nov 21 '19 at 10:09
  • Thank you, so so much. I have no words, thank you. I have learned a lot from this. – Teddy K Nov 21 '19 at 10:18
  • Still saving lives! – Codetard Sep 22 '20 at 11:13
0

Use "decodeIfPresent" variant method instead of decode method also you need to breakdown the ContentArticleTextEntries dictionary into individual keys:

struct ContentArticle: Decodable {
    var id: String
    var text: String?
    var locale: String?

    private enum CodingKeys: String, CodingKey {
        case id, text, locale
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decodeIfPresent(String.self, forKey: .id) ?? ""
        self.text = try container.decodeIfPresent(String.self, forKey: .text)
        self.locale = try container.decodeIfPresent(String.self, forKey: .locale) 
    }

}
Shubham Ojha
  • 461
  • 5
  • 17
0

If your models are like,

struct ContentArticle: Decodable {
    let textEntries: [String: ContentArticleTextEntries]
}

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

Then, you can simply access the data based on key like,

let response = try JSONDecoder().decode(ContentArticle.self, from: data)
let key = "summary"
print(response.textEntries[key])

Note: No need to write enum CodingKeys and init(from:) if there is no special handling while parsing the JSON.

PGDev
  • 23,751
  • 6
  • 34
  • 88
  • How do I take the JSON response and create `[String: ContentArticleTextEntries]`? `ContentArticleTextEntries` is the body of each field, I would need the key of each field to be added my dict? – Teddy K Nov 20 '19 at 10:39
  • You must be getting the `data` as response right? I've added how you can parse that data. In the above code, `response` is of type `[String: ContentArticleTextEntries]`. – PGDev Nov 20 '19 at 10:41
  • Ni I'm afraid not, vadian posted a comment further up that pointed to a solution, but I have been unable to make that work in this case. The trouble I am having is that I do no know the keys I am trying to decode, in my example for instance `summary` and `body` could be anything, however their props `ContentArticleTextEntries` will always be that shape. Any help would be awesome please. – Teddy K Nov 21 '19 at 08:21