1

I'm trying to understand how could I parse this multiple container JSON to an object. I've tried this approach (Mark answer), but he explain how to solve it using one-level container. For some reason I can't mimic the behaviour for multiple containers.

 {
     "graphql": {
        "shortcode_media": {
          "id": "1657677004214306744",
          "shortcode": "BcBQHPchwe4"
        }
     }
  }

class Post: Decodable {


    enum CodingKeys: String, CodingKey {
        case graphql // The top level "user" key
        case shortcode_media
    }

    enum PostKeys: String, CodingKey {
        case id
    }

    required init(from decoder: Decoder) throws {

        let values = try decoder.container(keyedBy: CodingKeys.self)

        let post = try values.nestedContainer(keyedBy: PostKeys.self, forKey: .shortcode_media)

        self.id = try post.decode(String.self, forKey: .id)

    }

    var id: String

}

I'm getting:

Swift.DecodingError.Context(codingPath: [], debugDescription: "Cannot get KeyedDecodingContainer<PostKeys> -- no value found for key \"shortcode_media\"", underlyingError: nil))

Any help will be much appreciated, thank you!

Roi Mulia
  • 5,626
  • 11
  • 54
  • 105

2 Answers2

5

As vadian notes, you haven't matched the JSON structure. There is no shortcode_media key at the top level like you've encoded in CodingKeys.

In order to decode this with a custom decoder, you will need to walk through each level and deal with it.

class Post: Decodable {

    enum CodingKeys: String, CodingKey {
        case graphql
    }

    enum GraphQLKeys: String, CodingKey {
        case shortcode_media
    }

    enum PostKeys: String, CodingKey {
        case id
    }

    required init(from decoder: Decoder) throws {
        // unload the top level
        let container = try decoder.container(keyedBy: CodingKeys.self)
        // Unload the graphql key
        let graphql = try container.nestedContainer(keyedBy: GraphQLKeys.self, forKey: .graphql)
        // unload the shortcode_media key
        let post = try graphql.nestedContainer(keyedBy: PostKeys.self, forKey: .shortcode_media)

        // Finally, unload the actual object
        self.id = try post.decode(String.self, forKey: .id)
    }

    var id: String

}
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • 1
    Hey Rob, thank you for responding. Vadian is correct, I misunderstood the relations within the JSON. A custom decoder is more appropriate for me in this scenario. As I mentioned as a commented within Vadian answer, I'm looking for a Post object at the end, easier for me to interact with the rest of the app. Thank you again! – Roi Mulia Jan 23 '18 at 21:51
  • How could we decode like this if there is a nested array. { "graphql": { "shortcode_media": ({ "id": "1657677004214306744", "shortcode": "BcBQHPchwe4" },{ "id": "xyzzy", "shortcode": "abc" }) } } – MasterGoGo Oct 16 '18 at 03:16
  • That isn't valid JSON; I assume you mean the `(` to be `[`. But it's the same. You'd just decode an array rather than a single value. If you're having trouble, this is a new question, however, and you should open a new question for it after searching for decoding nested arrays. – Rob Napier Oct 16 '18 at 12:24
1

Please read the JSON.

Any opening { is quasi a separator. The indentation of the JSON indicates also the hierarchy.

For clarity I removed all coding keys and left the variable names – which should be camelCased – unchanged.

struct Root : Decodable {
    let graphql : Graph

    // to access the `Media` object declare a lazy instantiated property

    lazy var media : Media = {
        return graphql.shortcode_media
    }()
}

struct Graph : Decodable {
    let shortcode_media : Media
}

struct Media : Decodable {
    let id: String
    let shortcode : String
}

let jsonString = """
{
    "graphql": {
        "shortcode_media": {
            "id": "1657677004214306744",
            "shortcode": "BcBQHPchwe4"
        }
    }
}
"""

do {       
    let data = Data(jsonString.utf8) 
    var result = try decoder.decode(Root.self, from: data)
    print(result.media)
} catch {
    print("error: ", error)
}

Writing a custom initializer with nestedContainer is more effort than creating the actual hierarchy.

Please paste the entire code in a Playground and check it out.

vadian
  • 274,689
  • 30
  • 353
  • 361
  • Hey Vadian! Thank you for responding and for the thoughtful answer. I did it before. The main issue I had is that the "result" object is of class "Root". So in order to access the sub-objects I need to go result.graphql.shortcode_media.id, for each of the properties, and than construct a new "Media" object. In Mark answer (it's not the accepted answer, attached at the OP), he shows that using containers and overriding "decode" init, we eventually call .decode(Media.self), which we get a "Media" object in return.Does it considered a better practice or am I missing the point?Thank you again sir! – Roi Mulia Jan 23 '18 at 21:44
  • It's a matter of taste. I updated the answer adding a lazy instantiated property to access the `Media` object directly from the `root` object. – vadian Jan 23 '18 at 21:52
  • Hey Vadian, sorry for not accepting your answer (I wish i could). It gave me a lot of insights, tho Mark answer (matter of taste), for the specific project is more suitable. I'll be sure to use the approach in different projects. Thank you again! – Roi Mulia Jan 23 '18 at 21:56
  • 1
    No worry. That's not a competition. *There's more than one way to skin a cat* – vadian Jan 23 '18 at 21:58
  • Hey Vadian! After a few weeks of trying out, I think your solution is better, as there is no need for container writing. Small question, what is the best approach to flatten this all sub-properties into one object? So if I had more containers etc', eventually I'll need an "Post" object who hold those properties. What would you recommend? – Roi Mulia Feb 03 '18 at 15:39
  • You can use either Rob's solution, **one** object *with* extra code, or mine, **multiple** objects *without* extra code, you cannot get **one** *without* – vadian Feb 03 '18 at 16:58
  • Hey Vadian! Thank you for responding, so I'll have to parse each struct to each object property basically? – Roi Mulia Feb 03 '18 at 17:00
  • Not necessariiy. Rob's solution parses two different JSON objects into one struct. – vadian Feb 03 '18 at 17:05
  • Vadian, I've decided to go with your approach, In my opinion, it's easier to maintain in case the API changes. Thank you! – Roi Mulia Feb 09 '18 at 19:23