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

SwiftUI中带认证的GET请求无法获取并解析REST API任务列表数据问题求助

SwiftUI中带认证的GET请求无法获取并解析REST API任务列表数据问题求助

嗨,我帮你看了下代码和返回的JSON响应,发现几个导致解析失败的关键问题,一个个来给你说:

首先是字段映射与类型不匹配的问题:

  • JSON里的主键是ID(字符串类型,比如"013"),但你的Job结构体里定义的是id: Int,既没有设置CodingKeys映射字段名,类型也不匹配,这肯定会解码失败;
  • JSON里的字段都是下划线分隔命名(比如job_addr_1、job_client_job_no),但Job结构体用的是驼峰命名(jobAddr1、jobClientJobNo),默认的JSONDecoder不会自动转换这种命名风格,需要手动映射或者设置解码器策略;

然后是可选性与类型错误:

  • 很多日期字段在JSON里是null或者空字符串(比如job_contractor_instructed是null,job_total_quote是空字符串),但你在Job里直接定义为非可选的Date类型,解码器无法把null/空字符串转成Date,会抛出错误;
  • 像job_type在JSON里是null,但你定义的jobType: Int是非可选类型,同样会解码失败;
  • 部分字段比如job_eicr_expiry_info在JSON里是空字符串,不是日期格式,你却定义成了Date类型,这也会导致解码失败;

最后是日期解码策略缺失:
JSON里的日期是yyyy-MM-dd格式的字符串,默认的JSONDecoder不会自动把这种字符串转成Date类型,需要手动设置日期解码策略。

下面是修改后的完整解决方案:

1. 修正Job结构体的字段映射与类型

struct Job: Codable, Identifiable {
    // 匹配JSON的字符串类型ID
    let id: String
    let jobUniqid: String?
    let jobClientJobNo: String?
    let jobPriority: String?
    let jobQuoteNo: String?
    let jobInvoiceNo: String?
    // 把非可选Int改成可选String(JSON里是字符串格式的数字)
    let jobStatus: String?
    let jobAgentId: String?
    let jobAddr1: String?
    let jobAddr2: String?
    let jobTownCity: String?
    let jobCounty: String?
    let jobPostcode: String?
    let jobContactName1: String?
    let jobContactNo1: String?
    let jobContactEmail1: String?
    let jobContactName2: String?
    let jobContactNo2: String?
    let jobContactEmail2: String?
    let jobSentBy: String?
    // 改成可选Int,匹配JSON的null值
    let jobType: Int?
    let jobContractorId: String?
    let jobDescription: String?
    let jobDescriptionShort: String?
    let jobBudget: String?
    let jobContractorBudget: String?
    let jobCreatedDate: String?
    let jobAgentNotes: String?
    let jobContractorNotes: String?
    let jobDynamoNotes : String?
    let jobNotes : String?
    // 所有日期字段改成可选Date
    let jobContractorInstructed : Date?
    let jobContractorAction : String?
    let jobContractorAccepted : Date?
    let jobContractorAcceptedDate : Date?
    let jobContractorDescription : String?
    let jobContractorPriceAgreed : String?
    let jobBookedDate : Date?
    let jobQuotedDate : Date?
    let jobAcceptedDate : Date?
    let jobStartDate : Date?
    let jobCompletionDate : Date?
    let jobInvoiceDate : Date?
    let jobOnholdDate : Date?
    let jobCancelledDate : Date?
    // 改成可选String,匹配JSON的空字符串
    let jobTotalQuote : String?
    let jobContractorCutPreVat : String?
    let jobContractorCut : String?
    let jobDynamoCut : String?
    let jobAgentsCut : String?
    let jobSophieCut : String?
    let jobInvoiceAmount : String?
    let jobPreVatAmount : String?
    let jobNetAmount : String?
    let jobVatAmount : String?
    let jobGscExpiryDate : Date?
    let jobFloorPlan : String?
    let jobVatCharged : String?
    let jobVatCollected : String?
    let jobInvoiceNotes : String?
    let jobContractorPaid : String?
    let jobInvoicePaid : String?
    let jobInvoicePaidEmail : String?
    let jobAgentsVat : String?
    let jobVatLiability : String?
    let jobContractorInvoice : String?
    let jobContractorInvoiceNo : String?
    let jobContactReason : String?
    let jobQuoteDescription : String?
    let jobContractorProceed : Date?
    let jobContractorProceedReceipt : String?
    let jobSendSms : Int?
    let jobDetailsOfGsc : String?
    let jobDetailsId : Int?
    let jobIntroducerId : Int?
    let jobIntroducerCommissionPaid : Date?
    let jobEicrExpiryDate : Date?
    // 改成可选String,匹配JSON的空字符串
    let jobEicrExpiryInfo : String?
    let jobEpcExpiryInfo : String?
    let jobGscExpiryInfo : String?
    let jobEpcExpiryDate : Date?
    let jobOnholdReason : String?
    let jobPartPaymentReceived : String?
    let jobContractorPartPaymentPaid : String?
    let jobContractorBalance : String?
    let jobQuoteRejectedDate : Date?
    let jobReminderSent : Data?
    let jobDepositInvoiced : String?
    let jobAmberList : String?
    
    // 手动映射JSON字段名
    enum CodingKeys: String, CodingKey {
        case id = "ID"
        case jobUniqid = "job_uniqid"
        case jobClientJobNo = "job_client_job_no"
        case jobPriority = "job_priority"
        case jobQuoteNo = "job_quote_no"
        case jobInvoiceNo = "job_invoice_no"
        case jobStatus = "job_status"
        case jobAgentId = "job_agent_id"
        case jobAddr1 = "job_addr_1"
        case jobAddr2 = "job_addr_2"
        case jobTownCity = "job_town_city"
        case jobCounty = "job_county"
        case jobPostcode = "job_postcode"
        case jobContactName1 = "job_contact_name_1"
        case jobContactNo1 = "job_contact_no_1"
        case jobContactEmail1 = "job_contact_email_1"
        case jobContactName2 = "job_contact_name_2"
        case jobContactNo2 = "job_contact_no_2"
        case jobContactEmail2 = "job_contact_email_2"
        case jobSentBy = "job_sent_by"
        case jobType = "job_type"
        case jobContractorId = "job_contractor_id"
        case jobDescription = "job_description"
        case jobDescriptionShort = "job_description_short"
        case jobBudget = "job_budget"
        case jobContractorBudget = "job_contractor_budget"
        case jobCreatedDate = "job_created_date"
        case jobAgentNotes = "job_agent_notes"
        case jobContractorNotes = "job_contractor_notes"
        case jobDynamoNotes = "job_dynamo_notes"
        case jobNotes = "job_notes"
        case jobContractorInstructed = "job_contractor_instructed"
        case jobContractorAction = "job_contractor_action"
        case jobContractorAccepted = "job_contractor_accepted"
        case jobContractorAcceptedDate = "job_contractor_accepted_date"
        case jobContractorDescription = "job_contractor_description"
        case jobContractorPriceAgreed = "job_contractor_price_agreed"
        case jobBookedDate = "job_booked_date"
        case jobQuotedDate = "job_quoted_date"
        case jobAcceptedDate = "job_accepted_date"
        case jobStartDate = "job_start_date"
        case jobCompletionDate = "job_completion_date"
        case jobInvoiceDate = "job_invoice_date"
        case jobOnholdDate = "job_onhold_date"
        case jobCancelledDate = "job_cancelled_date"
        case jobTotalQuote = "job_total_quote"
        case jobContractorCutPreVat = "job_contractor_cut_pre_vat"
        case jobContractorCut = "job_contractor_cut"
        case jobDynamoCut = "job_dynamo_cut"
        case jobAgentsCut = "job_agents_cut"
        case jobSophieCut = "job_sophie_cut"
        case jobInvoiceAmount = "job_invoice_amount"
        case jobPreVatAmount = "job_pre_vat_amount"
        case jobNetAmount = "job_net_amount"
        case jobVatAmount = "job_vat_amount"
        case jobGscExpiryDate = "job_gsc_expiry_date"
        case jobFloorPlan = "job_floor_plan"
        case jobVatCharged = "job_vat_charged"
        case jobVatCollected = "job_vat_collected"
        case jobInvoiceNotes = "job_invoice_notes"
        case jobContractorPaid = "job_contractor_paid"
        case jobInvoicePaid = "job_invoice_paid"
        case jobInvoicePaidEmail = "job_invoice_paid_email"
        case jobAgentsVat = "job_agents_vat"
        case jobVatLiability = "job_vat_liability"
        case jobContractorInvoice = "job_contractor_invoice"
        case jobContractorInvoiceNo = "job_contractor_invoice_no"
        case jobContactReason = "job_contact_reason"
        case jobQuoteDescription = "job_quote_description"
        case jobContractorProceed = "job_contractor_proceed"
        case jobContractorProceedReceipt = "job_contractor_proceed_receipt"
        case jobSendSms = "job_send_sms"
        case jobDetailsOfGsc = "job_details_of_gsc"
        case jobDetailsId = "job_details_id"
        case jobIntroducerId = "job_introducer_id"
        case jobIntroducerCommissionPaid = "job_introducer_commission_paid"
        case jobEicrExpiryDate = "job_eicr_expiry_date"
        case jobEicrExpiryInfo = "job_eicr_expiry_info"
        case jobEpcExpiryInfo = "job_epc_expiry_info"
        case jobGscExpiryInfo = "job_gsc_expiry_info"
        case jobEpcExpiryDate = "job_epc_expiry_date"
        case jobOnholdReason = "job_onhold_reason"
        case jobPartPaymentReceived = "job_part_payment_received"
        case jobContractorPartPaymentPaid = "job_contractor_part_payment_paid"
        case jobContractorBalance = "job_contractor_balance"
        case jobQuoteRejectedDate = "job_quote_rejected_date"
        case jobReminderSent = "job_reminder_sent"
        case jobDepositInvoiced = "job_deposit_invoiced"
        case jobAmberList = "job_amber_list"
    }
}

2. 修正loadData方法的解码逻辑

func loadData() {
    guard let url = URL(string: "https://api.badsey.app/\(MemberCheck)/API/dp_jobs/read.php?pageno=1&pagesize=1") else {
        print("Invalid URL")
        DispatchQueue.main.async {
            self.alertMessage = "Invalid API URL"
            self.showAlert = true
        }
        return
    }

    var request = URLRequest(url: url)
    request.httpMethod = "GET"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.setValue("Bearer \(TokenCheck)", forHTTPHeaderField: "Authorization")

    URLSession.shared.dataTask(with: request) { data, response, error in
        // 先处理错误和空数据的情况,避免强制解包崩溃
        guard let data = data, error == nil else {
            let errorMsg = error?.localizedDescription ?? "No data received from API"
            print("Request error: \(errorMsg)")
            DispatchQueue.main.async {
                self.alertMessage = errorMsg
                self.showAlert = true
            }
            return
        }
        
        print("---> API Response: \(String(data: data, encoding: .utf8) ?? "Invalid UTF8 data")")

        do {
            let decoder = JSONDecoder()
            // 设置日期解码策略,匹配JSON的yyyy-MM-dd格式
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "yyyy-MM-dd"
            decoder.dateDecodingStrategy = .formatted(dateFormatter)
            
            let decodedResponse = try decoder.decode(JobAPI.self, from: data)
            DispatchQueue.main.async {
                self.jobs = decodedResponse.document.records
            }
        } catch {
            let decodeError = "Decoding failed: \(error.localizedDescription)"
            print(decodeError)
            DispatchQueue.main.async {
                self.alertMessage = decodeError
                self.showAlert = true
            }
        }
    }.resume()
}

额外注意事项

  • 不要强制解包data!,如果网络请求失败或返回空数据会直接崩溃,一定要先判断数据是否存在;
  • 可以通过alert展示错误信息,方便调试;
  • 如果后续遇到新的解码错误,可以根据控制台打印的错误信息,针对性调整字段的类型或可选性。

按照上面的修改,应该就能正常解析API数据并展示在列表里了,有问题随时再问!


备注:内容来源于stack exchange,提问作者Badsey

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.22 15:40:28