基于URLSession并发处理后端验证错误及含未知键的服务器错误响应解码方案问询
处理含未知键的服务器错误响应解码问题
针对你遇到的JSON解码相关问题,我来逐个给出实用的解决方案:
1. 解码含未知键的错误对象的最佳方式
最类型安全且符合Swift Codable规范的方案,是定义专门的错误响应模型,用字典类型来容纳动态变化的错误字段。因为服务器返回的errors内键是动态的,但值都是字符串数组,所以可以这样定义模型:
// 错误响应的顶层模型 struct ErrorDetailModel: Codable { let data: ErrorData } // 错误数据模型,用字典接收动态键 struct ErrorData: Codable { let errors: [String: [String]] }
这样无论服务器返回login、email还是其他任意键,字典都能完美解码,同时保留了类型安全——你明确知道值是字符串数组,避免了Any类型的不确定性。
如果遇到更复杂的动态结构,也可以考虑用JSONSerialization直接解析成[String: Any],但Codable的方式更推荐,因为它能在编译期检查字段正确性,减少运行时错误。
2. 在post函数中处理这类错误
要处理这类错误,核心是先判断响应状态码,再分支执行解码逻辑,同时定义自定义错误类型来区分不同错误场景。
第一步:定义自定义错误枚举
先创建一个枚举来封装API调用中可能出现的所有错误:
enum APIError: Error, LocalizedError { case invalidResponse // 非HTTP格式的响应 case serverError([String: [String]]) // 服务器返回的业务错误 case decodingError(Error) // 数据解码失败 case networkError(Error) // 网络层错误 // 可选:实现LocalizedError提供用户友好的错误信息 var errorDescription: String? { switch self { case .invalidResponse: return "无效的服务器响应" case .serverError(let errors): let errorMessages = errors.values.flatMap { $0 }.joined(separator: " ") return "服务器错误: \(errorMessages)" case .decodingError(let error): return "数据解析失败: \(error.localizedDescription)" case .networkError(let error): return "网络连接错误: \(error.localizedDescription)" } } }
第二步:修改post扩展函数
在原有的post函数中,先检查HTTP状态码,成功状态(200-299)解码目标模型,错误状态则解码错误模型并抛出自定义错误:
extension URLSession { func post<T: Decodable, U: Encodable>( _ type: T.Type = T.self, data: U, from url: URL, keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .convertFromSnakeCase ) async throws -> T { var request = URLRequest(url: url) request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpMethod = "POST" let body = try JSONEncoder().encode(data) if let token = UserDefaults.standard.string(forKey: "AccessToken") { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } do { let (data, response) = try await upload(for: request, from: body) // 先验证是否为HTTP响应 guard let httpResponse = response as? HTTPURLResponse else { throw APIError.invalidResponse } // 根据状态码分支处理 if (200...299).contains(httpResponse.statusCode) { let decoder = JSONDecoder() decoder.keyDecodingStrategy = keyDecodingStrategy return try decoder.decode(T.self, from: data) } else { // 错误状态码,尝试解码错误响应 let decoder = JSONDecoder() decoder.keyDecodingStrategy = keyDecodingStrategy do { let errorResponse = try decoder.decode(ErrorDetailModel.self, from: data) throw APIError.serverError(errorResponse.data.errors) } catch { throw APIError.decodingError(error) } } } catch { throw APIError.networkError(error) } } }
3. 优化错误发生时的无效解码流程
你之前的代码在错误发生时仍会尝试解码目标模型,导致不必要的解码失败。通过上面的修改,我们已经彻底解决了这个问题:
- 先判断HTTP状态码,只有成功状态才会尝试解码
T.self(比如DetailModel<UserModel>) - 错误状态码会直接走错误响应解码逻辑,避免对目标模型的无效解码
- 所有错误都被封装成自定义的
APIError,上层调用者可以更清晰地处理不同错误场景
上层调用示例(login函数优化)
现在你的login函数可以针对性处理不同错误:
func login(email: String, password: String) async { let body = LoginRequest(email: email, password: password) guard let url = URL(string: "https://api.junoreader.com/api/auth/login") else { print("无效的请求地址") return } do { let response = try await URLSession.shared.post(DetailModel<UserModel>.self, data: body, from: url) setUser(data: response.data) } catch let error as APIError { print(error.localizedDescription) // 可以根据错误类型做更具体的处理,比如弹窗提示用户账号密码错误 if case .serverError(let errors) = error { if let loginErrors = errors["login"] { print("登录失败: \(loginErrors.joined())") } } } catch { print("未知错误: \(error)") } }
内容的提问来源于stack exchange,提问作者andre de waard
相关产品推荐
相关产品推荐

