Search code examples
jsonswiftweb-servicesparameterscodable

Post method generic codable API response not coming properly in swift


I have created APINetworkManagerAll here i have created serviceCall and calling that in ViewController. here postGenericCall2() is called and response also coming but my given param values not coming instead it says nil why? i am passing same values in postmn then response coming properly? where am i wrong? guide me please.

struct RequestObjectAll {
    var params: [String: Any]? = nil
    var method: HTTPMethod
    var urlPath: String
    var isTokenNeeded: Bool
    var isLoaderNeed: Bool = false
    var isErrorNeed: Bool = false
    var vc: UIViewController?
}

class APINetworkManagerAll: NSObject {
    static let sharedInstance = APINetworkManagerAll()
    
    fileprivate override init() {
        super.init()
    }
    
    func serviceCall<T: Decodable>(requestObject: RequestObjectAll, completion: @escaping (Result<T, Error>) -> Void) {
        if requestObject.isLoaderNeed {
            requestObject.vc?.showLoader()
        }
        
        guard let url = URL(string: requestObject.urlPath) else {
            if requestObject.isLoaderNeed {
                requestObject.vc?.hideLoader()
            }
            completion(.failure(NetworkError.invalidURL))
            return
        }
        
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = requestObject.method.rawValue
        
        guard let httpBody = try? JSONSerialization.data(withJSONObject: requestObject.params ?? ["" : ""], options: []) else {
                        return
                    }
        urlRequest.httpBody = httpBody
        
        let task = URLSession.shared.dataTask(with: urlRequest) { data, _, error in
            if requestObject.isLoaderNeed {
                requestObject.vc?.hideLoader()
            }
            
            if let error = error {
                completion(.failure(error))
                return
            }
            
            if let data = data {
                do {
                    let response = try JSONDecoder().decode(T.self, from: data)
                    completion(.success(response))
                } catch {
                    completion(.failure(error))
                }
            } else {
                let error = NSError(domain: "YourDomain", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data received"])
                completion(.failure(error))
            }
        }
        task.resume()
    }
}

And

func postGenericCall2(){        
    let param = ["name": "john",
                 "job": "AAA"
    ] 
    
    let requestObject = RequestObjectAll(
        params: param,
        method: .post,
        urlPath: "https://reqres.in/api/users",
        isTokenNeeded: false,
        isLoaderNeed: true,
        vc: self
    )
    APINetworkManagerAll.sharedInstance.serviceCall(requestObject: requestObject) { (result: Result<PostGenModelSmall, Error>) in
        switch result {
        case .success(let response):
            // Handle a successful response
            print("result of post service call.....: \(response)")
            
        case .failure(let error):
            // Handle the error
            print("Error decoding JSON: \(error)")
        }
    }        
}

struct PostGenModelSmall: Codable {
    let name: String
    let job: String
    let id: String
    let createdAt: String
}

error:

Error decoding JSON: keyNotFound(CodingKeys(stringValue: "name", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "name", intValue: nil) ("name").", underlyingError: nil))

EDIT: if i write service call like this then i am getting response as postman. so is there any issue in above my APINetworkManagerAll > serviceCall method while adding urlRequest.httpBody = httpBody body to urlRequest?

func postServiceCall(){
    let params: [String : Any] = ["name": "john", "job": "AAA"]
    
    var request = URLRequest(url: URL(string: "https://reqres.in/api/users")!)
    request.httpMethod = "POST"
    request.httpBody = try? JSONSerialization.data(withJSONObject: params, options: [])
    request.addValue("application/json", forHTTPHeaderField: "Content-Type")
    
    DispatchQueue.global().async {
    let session = URLSession.shared
    let task = session.dataTask(with: request, completionHandler: { data, response, error -> Void in
        print(response!)
        do {
            guard let data = data, error == nil else{
                print(error?.localizedDescription ?? "No data")
                return
            }
            let json = try JSONSerialization.jsonObject(with: data) as? [String : Any]
            print("post data with response....\(json)")
        } catch {
            print("error")
        }
    })
    task.resume()
}
}

o/p:

post data with response....Optional(["id": 582, "createdAt": 2024-03-12T06:28:51.904Z, "job": AAA, "name": john])


Solution

  • As a general rule, we should examine the response and make sure it is what we expected.

    In this case, your JSON response to the POST request with JSON body is very strange. It contains:

    {
        "{\"name\": \"john\",\"job\": \"AAA\"}": "",
        "id": "100",
        "createdAt": "2024-03-11T20:37:25.020Z"
    }
    

    It looks like it was not expecting JSON in the body of the request. It is a question of what your backend is expecting. In your case, as your particular backend appears to accept both application/x-www-form-urlencoded and application/json requests, you have two options:

    1. You can set the Content-Type header of your request to let it know that the payload is JSON:

      request.setValue("application/json", forHTTPHeaderField: "Content-Type")
      
      do {
          urlRequest.httpBody = try JSONSerialization.data(withJSONObject: requestObject.params ?? ["" : ""])
      } catch {
          completion(.failure(error))       // note, if you are going to exit, make sure to call completion handler
          return
      }
      
    2. You can change the request to be an application/x-www-form-urlencoded request:

      // for the sake of clarity, you might want to set `Content-Type`
      // even though it defaults to x-www-form-urlencoded
      //
      // request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
      
      request.httpBody = Data("name=joe&job=AAA".utf8)
      

      Note, your particular server defaults

    But, as your code stands, the request is not well-formed (in the absence of a Content-Type header of application/json, it assumes it is application/x-www-form-urlencoded, but the body is JSON), and not only did the backend not detect this, but it also returned a nonsensical response.

    You really should fix the backend so that a malformed request returns a non-200 response, e.g., a 400 status code. And, often (in dev environments, at least) we would include a body (with some consistent “error” JSON payload) in that response that indicates the nature of the error, namely that name and job were not successfully parsed from the request. The specifics of how the backend should validate requests and report errors is beyond the scope of this question, but FYI.


    If we replace the httpBody with an application/x-www-form-urlencoded sort of payload, like below, it worked correctly:

    request.httpBody = Data("name=john&job=AAA".utf8)
    

    It generated the response you were looking for:

    {
        "name":"john",
        "job":"AAA",
        "id":"859",
        "createdAt":"2024-03-11T20:29:44.446Z"
    }
    

    Note, if your request must be in application/x-www-form-urlencoded, make sure to property escape the values. See HTTP Request in Swift with POST method if doing this manually. Or use a framework, like Alamofire, which will greatly simply the creation of well-formed x-www-form-urlencoded requests.


    As an unrelated observation, I might suggest defining createdAt to be a Date:

    struct PostGenModelSmall: Codable {
        let name: String
        let job: String
        let id: String
        let createdAt: Date
    }
    

    And, then when decoding, set a date formatter for the JSONDecoder:

    let formatter = DateFormatter()
    formatter.locale = Locale(identifier: "en_US_POSIX")
    formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSX"
            
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .formatted(formatter)
    let response = try decoder.decode(PostGenModelSmall.self, from: data)
    

    Then, it will parse the date for you.


    By the way, you mentioned that you have a working Postman request. In Postman, you can tap on the </> button, choose “Swift – URLSession” and it will show you a simple example of what the Swift code might look like:

    postman screen snapshot - code generation

    Like most auto-generated code, the code is not great, but it will give you a starting place from which you can start to compare and contrast against your code. In this case, note the httpBody of the request (and the setting of the Content-Type header of the request, which is not required, but is best practice).

    Obviously, when you do this, your request might be a little different, but hopefully it illustrates how you can use Postman features to see what the Swift code might look like.