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'
.
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:
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.
I also used validate
to check the status code. You do not need to manually check it yourself.
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).
I also changed the return type to be Result<M, Error>
. Two things to note:
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
.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
}