Search code examples
iosswiftalamofirensjsonserialization

[NSJSONSerialization dataWithJSONObject:options:error:]: Invalid top-level type in JSON write


When I run the code, I get the error ---------------------- "[NSJSONSerialization dataWithJSONObject:options:error:]: Invalid top-level type in JSON write". ----------------------

class BaseAPI<T: TargetType> {
    
    func fetchData<M: Decodable>(target: T, responseClass: M.Type, completion:@escaping(Result<M?, NSError>) -> Void) {
        let method = Alamofire.HTTPMethod(rawValue: target.methods.rawValue)
        let headers = Alamofire.HTTPHeaders(target.headers ?? [:])
        let params = buildParams(task: target.task)
        AF.request(target.baseUrl + target.path, method: method, parameters: params.0, encoding: params.1, headers: headers).responseDecodable(of: M.self) { response in
            guard let statusCode = response.response?.statusCode else {
                //ADD Custom Error
                completion(.failure(NSError()))
                return
            }
            if statusCode == 200 {
                // Successful Request
                guard let jsonResponse = try? response.result.get() else {
                    completion(.failure(NSError()))
                    return
                }
                guard let theJSONData = try? JSONSerialization.data(withJSONObject: jsonResponse, options: []) else {
                    completion(.failure(NSError()))
                    return
                }
                guard let responseObj = try? JSONDecoder().decode(M.self, from: theJSONData) else {
                    completion(.failure(NSError()))
                    return
                }
                completion(.success(responseObj))
                
            } else {
                completion(.failure(NSError()))
            }
        }
        
        
    }
    
    private func buildParams(task: Task) -> ([String:Any], ParameterEncoding) {
        switch task {
            
        case .requestPlain:
            return ([:], URLEncoding.default)
        case .requestParameters(parameters: let parameters, encoding: let encoding):
            return (parameters, encoding)
        }
    }
}

instead of try? JSONSerialization.data(withJSONObject: jsonResponse, options: []) When I try the try? JSONSerialization.data(withJSONObject: jsonResponse, options: []) code, it gives the error No exact matches in call to class method 'jsonObject'.


Solution

  • The jsonResponse already is the decoded object, M. One cannot call JSONSerialization method data(withJSONObject:options:) with an arbitrary object. It only works with very specific types, namely dictionaries with strings, numbers, etc. (See a list of the acceptable types in the JSONSerialization documentation.) That is why you are receiving this error.


    That having been said, it begs the question why one would even attempt to re-encode the object back to a Data with JSONSerialization and re-decoding that Data with JSONDecoder back to a M. I would suggest eliminating this, simplifying the code to:

    func fetchData<M: Decodable>(target: T, responseClass: M.Type = M.self, completion: @escaping(Result<M, Error>) -> Void) {
        let method = Alamofire.HTTPMethod(rawValue: target.methods.rawValue)
        let headers = Alamofire.HTTPHeaders(target.headers ?? [:])
        let params = buildParams(task: target.task)
        AF.request(target.baseUrl + target.path, method: method, parameters: params.0, encoding: params.1, headers: headers)
            .validate()
            .responseDecodable(of: M.self) { response in
                switch response.result {
                case .success(let value): completion(.success(value))
                case .failure(let error): completion(.failure(error))
                }
            }
    }
    

    A few observations:

    1. As a minor refinement, I would advise making responseClass an optional parameter (as often the type can be inferred from the context). But I kept the parameter for those cases where the compiler cannot infer the type automatically.

    2. I also used validate to check the status code. You do not need to manually check it yourself.

    3. Also note, in the process of simplifying this, I have retired the use of try?. The try? is an antipattern, as that discards meaningful diagnostic information.

      But we always pass along the meaningful error: If the caller doesn't care about the details, it can disregard the specific error that was thrown. But in those cases where you actually have parsing errors, it is exceptionally useful to have the details (so you know which field(s) caused the problem).

    4. I also changed the return type to be Result<M, Error>. Two things to note:

      • The “success” does not have to be an optional M?, but rather could be a M. If it was successful, there’s no need to require callers to unwrap an optional which will never be nil.
      • The “failure” is Error, not NSError. Theoretically, it could be a AFError (though I do not generally like propagating Alamofire-specific types to the callers).

    FWIW, the Swift concurrency rendition might look like:

    func fetchData<M: Decodable>(target: T, responseClass: M.Type = M.self) async throws -> M {
        let params = buildParams(task: target.task)
    
        return try await AF.request(
            target.baseUrl + target.path,
            method: Alamofire.HTTPMethod(rawValue: target.methods.rawValue),
            parameters: params.0,
            encoding: params.1,
            headers: target.headers.flatMap { Alamofire.HTTPHeaders($0) }
        )
        .validate()
        .serializingDecodable(M.self)
        .value
    }