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

Swift中能否合并JSONSerialization与JSONDecoder实现统一API请求函数?

合并Swift API请求函数,同时支持Codable解码与原始JSON字典返回

我在Swift中实现了两个API请求函数:一个支持用Codable结构体解码返回数据,另一个返回未解码的[String: Any]类型JSON字典,用于通过Socket.io向服务端发送数据。现在想把这两个函数合并为一个,同时保留两种使用场景的能力。

原调用示例

场景1:解码为Codable结构体

let url = "https://remote-production.up.railway.app/getRoomId"
let apiService = APIService(urlString: url)
Task.detached {
    do {
        let response: RoomId = try await apiService.getJSON()
    }
}

场景2:获取未解码JSON用于Socket.io发送

Task {
    do {
        let responseData: [String: Any] = try await apiService.getSerializationJSON()
        let dataToSend: [String: Any] = ["requestId": requestId, "response": responseData]
        /// 向服务端客户端发送事件
        self.socket.emit("host_response", dataToSend)
        self.userSettings.recentConnections.append(userIp) // 将userIp添加到recentConnections列表
    } catch {
        print("API请求出错:\(error.localizedDescription)")
    }
}

原有APIService.swift代码

import Foundation

class APIService {
    let urlString: String
    let headersDict: [String: String]
    
    init(urlString: String, headersDict: [String: String] = [:]) {
        self.urlString = urlString
        self.headersDict = headersDict
    }
    
    func getSerializationJSON() async throws -> [String: Any] {
        guard let url = URL(string: urlString) else {
            throw APIError.invalidURL
        }
        
        // 设置请求对象:示例头、方法等
        var urlRequest = URLRequest(url: url)
        
        // 如果有自定义头则设置
        headersDict.forEach { key, value in
            urlRequest.setValue(value, forHTTPHeaderField: key)
        }
        
        do {
            let (data, response) = try await URLSession.shared.data(for: urlRequest)
            guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
                throw APIError.invalidResponseStatus
            }
            
            do {
                let decodedDictionary = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
                guard let jsonDictionary = decodedDictionary else {
                    throw APIError.corruptData
                }
                return jsonDictionary
            } catch {
                throw APIError.decodingError(error.localizedDescription)
            }
        } catch {
            throw APIError.dataTaskError(error.localizedDescription)
        }
    }
    
    func getJSON<T: Decodable>(dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate,
                               keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys) async throws -> T {
        guard
            let url = URL(string: urlString)
        else {
            throw APIError.invalidURL
        }
        do {
            let (data, response) = try await URLSession.shared.data(from: url)
            guard
                let httpResponse = response as? HTTPURLResponse,
                httpResponse.statusCode == 200
            else {
                throw APIError.invalidResponseStatus
            }
            let decoder = JSONDecoder()
            decoder.dateDecodingStrategy = dateDecodingStrategy
            decoder.keyDecodingStrategy = keyDecodingStrategy
            do {
                let decodedData = try decoder.decode(T.self, from: data)
                return decodedData
            } catch {
                throw APIError.decodingError(error.localizedDescription)
            }
        } catch {
            throw APIError.dataTaskError(error.localizedDescription)
        }
    }
}

enum APIError: Error, LocalizedError {
    case invalidURL
    case invalidResponseStatus
    case dataTaskError(String)
    case corruptData
    case decodingError(String)
    
    var errorDescription: String? {
        switch self {
        case .invalidURL:
            return NSLocalizedString("端点URL无效", comment: "")
        case .invalidResponseStatus:
            return NSLocalizedString("API未能返回有效响应。", comment: "")
        case .dataTaskError(let string):
            return string
        case .corruptData:
            return NSLocalizedString("提供的数据似乎已损坏", comment: "")
        case .decodingError(let string):
            return string
        }
    }
}

合并后的实现方案

你可以抽离重复的网络请求逻辑为私有方法,用一个泛型函数作为统一入口,根据返回类型自动选择解码方式,同时兼容两种使用场景。

修改后的APIService代码

import Foundation

class APIService {
    let urlString: String
    let headersDict: [String: String]
    
    init(urlString: String, headersDict: [String: String] = [:]) {
        self.urlString = urlString
        self.headersDict = headersDict
    }
    
    // 统一请求入口,支持返回Codable类型或[String: Any]字典
    func request<T>(dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate,
                    keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys) async throws -> T {
        // 先获取网络返回的原始数据
        let data = try await fetchRawData()
        
        // 根据返回类型T选择对应的解析方式
        if T.self == [String: Any].self {
            guard let jsonDict = try JSONSerialization.jsonObject(with: data) as? T else {
                throw APIError.corruptData
            }
            return jsonDict
        } else if let decodableType = T.self as? Decodable.Type {
            let decoder = JSONDecoder()
            decoder.dateDecodingStrategy = dateDecodingStrategy
            decoder.keyDecodingStrategy = keyDecodingStrategy
            guard let decodedResult = try decoder.decode(decodableType, from: data) as? T else {
                throw APIError.decodingError("无法解析为目标类型")
            }
            return decodedResult
        } else {
            throw APIError.decodingError("不支持的返回类型")
        }
    }
    
    // 私有方法:封装重复的网络请求逻辑
    private func fetchRawData() async throws -> Data {
        guard let url = URL(string: urlString) else {
            throw APIError.invalidURL
        }
        
        var urlRequest = URLRequest(url: url)
        headersDict.forEach { key, value in
            urlRequest.setValue(value, forHTTPHeaderField: key)
        }
        
        let (data, response) = try await URLSession.shared.data(for: urlRequest)
        guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
            throw APIError.invalidResponseStatus
        }
        
        return data
    }
}

enum APIError: Error, LocalizedError {
    case invalidURL
    case invalidResponseStatus
    case dataTaskError(String)
    case corruptData
    case decodingError(String)
    
    var errorDescription: String? {
        switch self {
        case .invalidURL:
            return NSLocalizedString("端点URL无效", comment: "")
        case .invalidResponseStatus:
            return NSLocalizedString("API未能返回有效响应。", comment: "")
        case .dataTaskError(let string):
            return string
        case .corruptData:
            return NSLocalizedString("提供的数据似乎已损坏", comment: "")
        case .decodingError(let string):
            return string
        }
    }
}

合并后的调用方式

场景1:解码为Codable结构体

let url = "https://remote-production.up.railway.app/getRoomId"
let apiService = APIService(urlString: url)
Task.detached {
    do {
        let response: RoomId = try await apiService.request()
        // 处理解码后的RoomId实例
    } catch {
        print(error.localizedDescription)
    }
}

场景2:获取未解码JSON用于Socket.io发送

Task {
    do {
        let responseData: [String: Any] = try await apiService.request()
        let dataToSend: [String: Any] = ["requestId": requestId, "response": responseData]
        self.socket.emit("host_response", dataToSend)
        self.userSettings.recentConnections.append(userIp)
    } catch {
        print("API请求出错:\(error.localizedDescription)")
    }
}

实现说明

  1. 代码复用:将URL校验、请求头设置、网络请求、状态码校验这些重复逻辑封装到fetchRawData私有方法中,减少冗余代码。
  2. 泛型统一入口:request<T>方法通过判断返回类型T,自动切换解析方式:
    • 当T是[String: Any]时,使用JSONSerialization解析原始JSON
    • 当T遵循Decodable时,使用JSONDecoder解码为目标结构体
  3. 兼容原有习惯:调用方式和之前几乎一致,仅需将原方法名替换为request,无需额外学习成本。

内容的提问来源于stack exchange,提问作者Pastor J.G Seigle

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 21:17:01