Search code examples
iosjsonswiftjsondecoder

Parse remote JSON without creating models in Swift, in a single function


My app must fetch JSON via HTTPS request, and then parse that data.

What I'm looking for is to have a "one function rules all" approach to it, like so.

func call(endpoint: String) {
   let url = URL(string: "https://example.com/api/"+endpoint)

   URLSession.shared.dataTask(with: url!) { (data, response, error) in
      // Parse JSON here without using a model.
      // Just convert the data into a JSON object and return it
   }
}

Every single example code I've come across seems to me that you need to make a model with the expected values, and then do this:

JSONDecoder().decode(ModelHere.self, from: data)

This means that I need to make a new function for each endpoint.

Why not just use a separate function for each endpoint?

I have a lot of endpoints that could be called from within a function.

Creating a new function for each endpoint to pass it a separate model to then parse the data from the response ... that is a lot of junk to sift through in the code.

I'm sure there is a better way to do it that is just obscure, or I'm missing.

Essentially, I'm looking for this:

a function needs remote data to load a view -> fires call('endpoint/example/') -> call() returns the json object -> the function can now take the data it needs

Multiple processor functions, one call() function. I hope I'm making sense.

Thanks


Solution

  • That's what generic is for, for instance:

    You can define T as a your date model which is a generic type, and you fetch it and return it with completion. Making T as codable so any model that conforms codable will make this call.

    func call<T: Codable>(endpoint: String, completion: (T) -> ()) {
       let url = URL(string: "https://example.com/api/"+endpoint)
    
       URLSession.shared.dataTask(with: url!) { (data, response, error) in
          // Parse JSON here without using a model.
          // Just convert the data into a JSON object and return it
          let model = JSONDecoder().decode(T.self, from: data)
          completion(model)
       }
    }
    
    

    And when you call the function:

    call(endpoint: "something") { model: ModelHere in 
       // Use your model
    }
    

    So you don't have to make separate functions for every model you have.

    ** Code not been tested, just as an idea

    EDIT: Here is an example with feedback from comment (Thank you @Leo)

    private var host = "https://jsonplaceholder.typicode.com/"
        
    enum NetworkError: Error {
        case invalidUrl
        case invalidData
        case underlying(_ error: Error)
    }
    
    func call<T: Decodable>(endpoint: String,
                            completion: @escaping (Result<T, NetworkError>) -> ()) -> Bool
    {
        guard let url = URL(string: host+endpoint) else {
            completion(.failure(.invalidUrl))
            return false
        }
        
        URLSession.shared.dataTask(with: url) { (data, response, error) in
            guard let data = data else {
                return completion(.failure(.invalidData))
            }
            do {
                let model = try JSONDecoder().decode(T.self, from: data)
                completion(.success(model))
            } catch let error {
                print(error.localizedDescription)
                completion(.failure(.underlying(error)))
            }
        }.resume()
        return true
    }
    

    And when you call the functions:

    struct SampleModel: Decodable {
        let id: Int
        let content: String
    }
    
    func sample() {
        let isSuccess = call(endpoint: "fetchSampleModelUrl") { (result: Result<[SampleModel], NetworkError>) in
            switch result {
            case .success(let model):
                print("\(model)")
            case .failure(let error):
                print("\(error)")
            }
        }
    }