You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 09:57:37