swiftswiftuidateformatter

DateFormatter date decoding error in Swift from JSON data


This is the error I get

Invalid data: typeMismatch(Swift.Double, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "Services", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "NextBus", intValue: nil), CodingKeys(stringValue: "EstimatedArrival", intValue: nil)], debugDescription: "Expected to decode Double but found a string/data instead.", underlyingError: nil))

When I try to print the estimatedArrival in the SwiftUI View as a Text() converted from the date format 2022-08-29T12:06:28+08:00 which is in "yyyy-MM-dd’T’HH:mm:ssZ" format.

However when I change to estimatedArrival: String instead of Date, it doesn't the error anymore.

import SwiftUI

import Foundation

// MARK: - Welcome
struct BusTimings: Codable {
    let busStopCode: String
    let services: [Service]

    enum CodingKeys: String, CodingKey {
        case busStopCode = "BusStopCode"
        case services = "Services"
    }
}

// MARK: - Service
struct Service: Codable {
    let serviceNo, serviceOperator: String
    let nextBus, nextBus2, nextBus3: NextBus

    enum CodingKeys: String, CodingKey {
        case serviceNo = "ServiceNo"
        case serviceOperator = "Operator"
        case nextBus = "NextBus"
        case nextBus2 = "NextBus2"
        case nextBus3 = "NextBus3"
    }
}

// MARK: - NextBus
struct NextBus: Codable {
    let originCode, destinationCode: String
    let estimatedArrival: Date
    let latitude, longitude, visitNumber, load: String
    let feature, type: String

    enum CodingKeys: String, CodingKey {
        case originCode = "OriginCode"
        case destinationCode = "DestinationCode"
        case estimatedArrival = "EstimatedArrival"
        case latitude = "Latitude"
        case longitude = "Longitude"
        case visitNumber = "VisitNumber"
        case load = "Load"
        case feature = "Feature"
        case type = "Type"
    }
}




struct StopView: View {
    let item: BusStop
    let dateFormatter = DateFormatter()
    
    @State private var value = [Service]()
    //@State private var busStop = [BusStop]()
    //var locations = [Locations]()
    var body: some View {
        
        List(value, id: \.serviceNo) { item in
            NavigationLink(destination:Text(item.serviceNo)) {
                VStack(alignment: .leading) {
                    Text(item.serviceNo)
                        .font(.largeTitle)
                    Text(dateFormatter.string(from: item.nextBus.estimatedArrival))
                       // .bold()

                }
            }
        }
        .task {
            await loadData()
        }
        .navigationTitle(item.Description)
        .navigationBarTitleDisplayMode(.inline)
        
    }
    
    //let url = Bundle.main.url(forResource: "data", withExtension: "json")!

    
    func loadData() async {
        let busStopCode = item.BusStopCode
        let accountKey = "xxx"
        guard let url = URL(string: String("http://datamall2.mytransport.sg/ltaodataservice/BusArrivalv2" + "?AccountKey=" + accountKey + "&BusStopCode=" + busStopCode))
        else { print("Invalid URL")
            return
        }
        
        
        //print(dateFormatter.string(from: Date.now))

        
        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        request.addValue(accountKey, forHTTPHeaderField: "AccountKey")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        
        do {
            let (data, _) = try await URLSession.shared.data(for: request) // <-- here
            //print(String(data: data, encoding: .utf8)) // <-- here
            let decodedResponse = try JSONDecoder().decode(BusTimings.self, from: data)
            value = decodedResponse.services
        } catch {
            print("Invalid data: \(error)")
        }
    }
}

Thank you for reading through this! I appreciate your help!


Solution

  • try this decoder (in loadData()) or some variation of it, to decode your String date into a real Date:

    do {
        let (data, _) = try await URLSession.shared.data(for: request)
        let decoder = JSONDecoder() // <-- here
        decoder.dateDecodingStrategy = .formatted(formatter) // <-- here
        let decodedResponse = try decoder.decode(BusTimings.self, from: data)
        value = decodedResponse.services
    } catch {
        print("Invalid data: \(error)")
    }
    

    Where you have:

    let formatter = DateFormatter()
    formatter.locale = Locale(identifier: "en_US_POSIX") // <-- todo
    formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
    

    Your date format "yyyy-MM-dd'T'HH:mm:ssZ" looks like iso8601, so you can also try:

     decoder.dateDecodingStrategy = .iso8601
    

    EDIT-1: the test code that shows my answer works with the official data.

    from the official website: https://datamall.lta.gov.sg/content/datamall/en/dynamic-data.html using the Bus Arrival data, downloaded as json and shown here as example.

    Here is the test code that shows, that both the

      decoder.dateDecodingStrategy = .iso8601
    

    and the

     decoder.dateDecodingStrategy = .formatted(formatter)
    

    can decode the EstimatedArrival dates from the official data set.

    Using

      formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
    

    also works.

    struct ContentView: View {
        @State private var services = [Service]()
        
        let formatter: DateFormatter = {
            let frmt = DateFormatter()
            frmt.timeZone = TimeZone(identifier: "Asia/Singapore")
            frmt.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" // "yyyy-MM-dd'T'HH:mm:ssZ" also works
            return frmt
        }()
        
        var body: some View {
            NavigationView {
                List(services, id: \.serviceNo) { service in
                    NavigationLink(destination: Text(service.serviceNo) ) {
                        VStack(alignment: .leading) {
                            Text(service.serviceNo).font(.largeTitle)
                            Text(formatter.string(from: service.nextBus.estimatedArrival)).bold()
                        }
                    }
                }
            }
            .onAppear {
                let json = """
    {
        "odata.metadata": "http://datamall2.mytransport.sg/ltaodataservice/$metadata#BusArrivalv2/@Element",
        "BusStopCode": "20251",
        "Services": [
            {
                "ServiceNo": "176",
                "Operator": "SMRT",
                "NextBus": {
                    "OriginCode": "10009",
                    "DestinationCode": "45009",
                    "EstimatedArrival": "2020-02-12T14:09:11+08:00",
                    "Latitude": "1.301219",
                    "Longitude": "103.762202",
                    "VisitNumber": "1",
                    "Load": "SEA",
                    "Feature": "WAB",
                    "Type": "DD"
                },
                "NextBus2": {
                    "OriginCode": "10009",
                    "DestinationCode": "45009",
                    "EstimatedArrival": "2020-02-12T14:21:19+08:00",
                    "Latitude": "1.2731256666666666",
                    "Longitude": "103.800273",
                    "VisitNumber": "1",
                    "Load": "SEA",
                    "Feature": "WAB",
                    "Type": "DD"
                },
                "NextBus3": {
                    "OriginCode": "10009",
                    "DestinationCode": "45009",
                    "EstimatedArrival": "2020-02-12T14:44:30+08:00",
                    "Latitude": "0",
                    "Longitude": "0",
                    "VisitNumber": "1",
                    "Load": "SEA",
                    "Feature": "WAB",
                    "Type": "DD"
                }
            },
            {
                "ServiceNo": "78",
                "Operator": "TTS",
                "NextBus": {
                    "OriginCode": "28009",
                    "DestinationCode": "28009",
                    "EstimatedArrival": "2020-02-12T14:09:09+08:00",
                    "Latitude": "1.3069268333333333",
                    "Longitude": "103.73333",
                    "VisitNumber": "1",
                    "Load": "SEA",
                    "Feature": "WAB",
                    "Type": "DD"
                },
                "NextBus2": {
                    "OriginCode": "28009",
                    "DestinationCode": "28009",
                    "EstimatedArrival": "2020-02-12T14:26:17+08:00",
                    "Latitude": "1.3086495",
                    "Longitude": "103.76608433333334",
                    "VisitNumber": "1",
                    "Load": "SEA",
                    "Feature": "WAB",
                    "Type": "DD"
                },
                "NextBus3": {
                    "OriginCode": "28009",
                    "DestinationCode": "28009",
                    "EstimatedArrival": "2020-02-12T14:36:38+08:00",
                    "Latitude": "1.3126545",
                    "Longitude": "103.7666475",
                    "VisitNumber": "1",
                    "Load": "SEA",
                    "Feature": "WAB",
                    "Type": "DD"
                }
            }
        ]
    }
    """
                // simulated api response
                let data = json.data(using: .utf8)!
                do {
                    let decoder = JSONDecoder()
                    // decoder.dateDecodingStrategy = .iso8601  // <-- this also works
                    decoder.dateDecodingStrategy = .formatted(formatter)
                    let decoded = try decoder.decode(BusTimings.self, from: data)
                    print("\n---> decoded: \n \(decoded)")
                    services = decoded.services
                } catch {
                    print("==> decoding error: \(error)")
                }
            }
        }
        
    }
    
    struct BusTimings: Codable {
        let busStopCode: String
        let services: [Service]
        
        enum CodingKeys: String, CodingKey {
            case busStopCode = "BusStopCode"
            case services = "Services"
        }
    }
    
    // MARK: - Service
    struct Service: Codable {
        let serviceNo, serviceOperator: String
        let nextBus, nextBus2, nextBus3: NextBus
        
        enum CodingKeys: String, CodingKey {
            case serviceNo = "ServiceNo"
            case serviceOperator = "Operator"
            case nextBus = "NextBus"
            case nextBus2 = "NextBus2"
            case nextBus3 = "NextBus3"
        }
    }
    
    // MARK: - NextBus
    struct NextBus: Codable {
        let originCode, destinationCode: String
        let estimatedArrival: Date
        let latitude, longitude, visitNumber, load: String
        let feature, type: String
        
        enum CodingKeys: String, CodingKey {
            case originCode = "OriginCode"
            case destinationCode = "DestinationCode"
            case estimatedArrival = "EstimatedArrival"
            case latitude = "Latitude"
            case longitude = "Longitude"
            case visitNumber = "VisitNumber"
            case load = "Load"
            case feature = "Feature"
            case type = "Type"
        }
    }