mikalooch
mikalooch

Reputation: 91

Parsing the Reddit JSON with Swift Codable

I am having a minor issue parsing the JSON from reddit.

Simplified reddit JSON response:

{
    "kind": "Listing",
    "data": {
        "after": "t3_c2wztu",
        "before": null,
        "children": [
            {
                "data": {
                    "all_awardings": [],
                    "approved_at_utc": null,
                    "approved_by": null
                },
                "kind": "t3"
            }
        ],
        "dist": 25,
        "modhash": ""
    }
}

I simplified the JSON as it can get 4000 lines long, mostly repeating children objects.

When we look at the second "data" key inside the "children" object, that's when we get all the needed data, like images, usernames, etc..

My structs in Swift are as follows: Seeing that the JSON is nested, I believe I need to create either nested structs or multiple structs to help drill down through the JSON. Am I correct I that?

struct Model: Decodable {
    let data : Children?
    enum CodingKeys: String, CodingKey {
        case data = "data"
    }
}

struct Children: Decodable {
    let data: [Child]?
    enum CodingKeys: String, CodingKey {
        case data = "data"
    }
}

//MARK - there is MORE than author and full name, we are just doing this for ease of parsing.
struct Child: Decodable {
    let author : String?
    let authorFullname : String?
    enum CodingKeys: String, CodingKey {
        case author = "author"
        case authorFullname = "author_fullname"
    }
     init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)

        author = try values.decodeIfPresent(String.self, forKey: .author)
        authorFullname = try values.decodeIfPresent(String.self, forKey: .authorFullname)
     }
}

Now here is my URLSession call:

class NetworkManager {

    static var shared: NetworkManager = { return NetworkManager() }()
    private init(){}

    public func getFrontPage(completion: @escaping (Model?) -> Void) {
        guard let url = URL(string: "https://www.reddit.com/r/all/.json") else { fatalError("Invalid URL") }
        URLSession.shared.dataTask(with: url) { data, response, err in
            if(err != nil) {
                completion(nil)
            }
            guard let httpResponse = response as? HTTPURLResponse else { fatalError("URL Response Error") }
            switch httpResponse.statusCode {
            case 200:
                if let data = data {
                    do {
                    let data = try JSONDecoder().decode(Model.self, from: data)
                    completion(data)
                    } catch {
                        print("ERROR DECODING JSON \(err.debugDescription)")
                    }
                }
                break
            default:
                completion(nil)
                break
            }
        }.resume()
    }

And finally in ViewDidLoad:

 var redditData = [Model]()

    override func viewDidLoad() {
        super.viewDidLoad()
        NetworkManager.shared.getFrontPage { (items) in
           // self.redditData = items?.data
            print(items)
            DispatchQueue.main.async {
                // update the UI
            }
        }
    }

Heres my current error I can't get past:

self.redditData = items?.data gives me an error:

Cannot assign value of type 'Children?' to type '[Model]'

Questions: 1. is my struct layout right? Don't worry about the actual data, like usernames and all, I shortened it for this post.

  1. am I "drilling" down through the JSON correctly in my network manager function?

  2. How do I fix my error in my viewController class?

Sorry for such a long topic!!

Upvotes: 2

Views: 475

Answers (1)

idz
idz

Reputation: 12988

You've just got your types mixed up a little.

The type of your completion for getFrontPage is (Model?) -> Void

So let's look at where you use it.

    NetworkManager.shared.getFrontPage { (items) in
        // items is Model? so...
        // items?.data is of type Children? 
        // BUT self.redditData is of type [Model]
        // self.redditData = items?.data
        print(items)
        DispatchQueue.main.async {
            // update the UI
        }
    }

The model structs you showed didn't match the example JSON. Here's some structs that will work. I'm not familiar with the Reddit API so these may need to be tweaked.

struct Model : Decodable {
    let kind: String
    let data: ListingData
}

struct ListingData: Decodable {
    let after: String
    let before: String?
    let children: [Child]
    let dist: Int
    let modhash: String
}

struct ChildData : Decodable {
    let allAwardings: [String]
    let approvedAtUtc: String?
    let approvedBy: String?
}
struct Child: Decodable {
    let data: ChildData
    let kind: String?
}

You can avoid all the CodingKey stuff if you use .convertFromSnakeCase on JSONDecoder. Just change getFrontPage to decode like this:

    let decoder = try JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let model = try decoder.decode(Model.self, from: data)
    completion(model)

Now, just make sure the types match for self.redditData and the field on the model you are interested in!

    var self.redditData = [Child]()

    NetworkManager.shared.getFrontPage { (model) in
        self.redditData = model?.data.children ?? []
        print(items)
        DispatchQueue.main.async {
            // update the UI
        }
    }

Hope this helps!

Upvotes: 3

Related Questions