UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: Failing to decode a JSON from URL :(

Forums > SwiftUI

Hello Everyone,

I'm close to throwing my laptop out the window, I'm new to Swift and have been doing a number of tutorials/examples decoding JSON frile from URL and loading it inot a list.

It's all be fine and beautify as the JSON files in the examples are all { key: value } now that I'm trying to do my own project the JSON file I'm dealing with is of this type, whic I'm struggling to decode.

Thank you in advance :)

JSON Recod from URL


{
  "departures": [
    {
      "stop_id": 1190,
      "route_id": 7,
      "run_id": 950000,
      "run_ref": "950000",
      "direction_id": 1,
      "disruption_ids": [
        266155
      ],
      "scheduled_departure_utc": "2023-01-12T17:59:00Z",
      "estimated_departure_utc": null,
      "at_platform": false,
      "platform_number": "1",
      "flags": "",
      "departure_sequence": 0
    },
 ],
  "stops": {},
  "routes": {},
  "runs": {},
  "directions": {},
  "disruptions": {},
  "status": {
    "version": "3.0",
    "health": 1
  }
}

My Data Struct for reading the JSON

struct Departures: Codable {
    let departures: [Departure]
    let stops, routes, runs, directions: Int

}

struct Departure: Codable {
    let stopID, routeID, runID: Int
    let runRef: String
    let directionID: Int
    let disruptionIDS: [Int]
    let scheduledDepartureUTC: Date
    let estimatedDepartureUTC: Date?
    let atPlatform: Bool
    let platformNumber: String?
    let flags: String
    let departureSequence: Int
}

My code to fetc/decode JSON and load into List

import SwiftUI

struct ContentView: View {

    @State var results = [Departure]()

    var dateFormatter: DateFormatter {
        let formatter = DateFormatter()
        formatter.timeStyle = .short
        return formatter
    }

    var body: some View {
        List(results, id: \.runRef) { item in
            VStack(alignment: .leading) {
                //  Text(dateFormatter.string(from:item.scheduledDepartureUTC))
                Text(item.directionID)
            }
        }
        }
    struct DepartureFetch {

        enum DepartureFetchError: Error {
            case invalidURL
            case missingData
        }

        static func fetchAlbumWithAsyncURLSession() async throws -> [Departure] {

            guard let url = URL(string: "https://blahblah.json") else {
                throw DepartureFetchError.invalidURL
            }

            // Use the async variant of URLSession to fetch data
            let (data, _) = try await URLSession.shared.data(from: url)

            // Parse the JSON data
            let ptvResult = try JSONDecoder().decode([Departure].self, from: data)
            return ptvResult.departures
        }
    }
}

Errors I'm getting is:

swift "typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: \"Expected to decode Array<Any> but found a dictionary instead.\", underlyingError: nil))""

I've tried to change the code to be departure.self in the JSONDecoder but that thows other errors.

1      

There are a number of issues here.

  1. On line 18 of your JSON, you cannot have a trailing , on the last item in a JSON array.
  2. You haven't properly mapped the JSON key names (e.g., stop_id) to the Departure property names (stopID). You need to have a CodingKeys enum to map those. You could use a keyDecodingStrategy with a value of convertFromSnakeCase, but then you would need to name your properties like stopId instead of stopID. Whichever way you choose to handle this is up to you, but you need to handle it.
  3. On line 12 of your JSON, your date is in ISO8601 format, but the default date decoding strategy for Codable is to represent dates as a Double. You need to set the dateDecodingStrategy of your JSONDecoder to .iso8601 so this will work properly.
  4. In your Departures struct, your stops, routes, runs, and directions properties are Ints, but in the JSON you have posted, they are objects (i.e., dictionaries or structs). You'll get an error trying to decode this because the system is expecting an Int but it is finding an empty Dictionary instead. I'm not sure what these values should actually be, so I used an [Int:Int] dictionary.
  5. Finally, when you decode, you need to use Departures.self instead of [Departure].self because Departures is a singular entity in your JSON, not an array of Departure items.

I think that should be all it takes to get you going. If you still find yourself having errors, please post back here and we'll work it out.

2      

And here is a simple example incorporating the changes I mentioned. Obviously, since I don't have everything necessary to fetch your JSON from whatever API it is coming from, I had to improvise, but this should be sufficient to give you an idea how to proceed. Feel free to ask questions.

import SwiftUI

let departuresData = """
{
  "departures": [
    {
      "stop_id": 1190,
      "route_id": 7,
      "run_id": 950000,
      "run_ref": "950000",
      "direction_id": 1,
      "disruption_ids": [
        266155
      ],
      "scheduled_departure_utc": "2023-01-12T17:59:00Z",
      "estimated_departure_utc": null,
      "at_platform": false,
      "platform_number": "1",
      "flags": "",
      "departure_sequence": 0
    }
 ],
  "stops": {},
  "routes": {},
  "runs": {},
  "directions": {},
  "disruptions": {},
  "status": {
    "version": "3.0",
    "health": 1
  }
}
""".data(using: .utf8)!

struct Departures: Codable {
    let departures: [Departure]
    let stops, routes, runs, directions: [Int:Int]

}

struct Departure: Codable {
    let stopID, routeID, runID: Int
    let runRef: String
    let directionID: Int
    let disruptionIDS: [Int]
    let scheduledDepartureUTC: Date
    let estimatedDepartureUTC: Date?
    let atPlatform: Bool
    let platformNumber: String?
    let flags: String
    let departureSequence: Int

    enum CodingKeys: String, CodingKey {
        case stopID = "stop_id"
        case routeID = "route_id"
        case runID = "run_id"
        case runRef = "run_ref"
        case directionID = "direction_id"
        case disruptionIDS = "disruption_ids"
        case scheduledDepartureUTC = "scheduled_departure_utc"
        case estimatedDepartureUTC = "estimated_departure_utc"
        case atPlatform = "at_platform"
        case platformNumber = "platform_number"
        case flags = "flags"
        case departureSequence = "departure_sequence"
    }
}

struct DeparturesView: View {
    @State private var departures: [Departure] = []

    var body: some View {
        List {
            //I use runRef to identify each Departure, since you haven't made it Identifiable
            ForEach(departures, id: \.runRef) { dep in
                VStack(alignment: .leading) {
                    Text("Route: \(dep.routeID), Stop: \(dep.stopID)")
                    Text(dep.scheduledDepartureUTC, format: .dateTime)
                        .font(.subheadline)
                }
            }
        }
        .onAppear {
            decodeDepartures()
        }
    }

    func decodeDepartures() {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601

        do {
            let result = try decoder.decode(Departures.self, from: departuresData)
            departures = result.departures
        } catch {
            print(error)
            departures = []
        }
    }
}

struct DeparturesView_Previews: PreviewProvider {
    static var previews: some View {
        DeparturesView()
    }
}

2      

Hi @roosterboy

Thank you for your time, and a massive thank you for helping me get this to work :)

Your code works a treat, I've just changed it to parse the remote URL.

Appologies for not sharing the URL, It needs a username/password to access.

Again thank you! Hope I can do the same for someone in the community

1      

If you use Ducky (which you can change the Property Naming style) for the model. I have commented out the properties and objects that are empty as do not know what in them.

struct Departure: Codable {
    let stopId: Int
    let routeId: Int
    let runId: Int
    let runRef: String
    let directionId: Int
    let disruptionIds: [Int]
    let scheduledDepartureUtc: Date
    let estimatedDepartureUtc: Date? // <- have to change this as Ducky given ANY?
    let atPlatform: Bool
    let platformNumber: String
    let flags: String
    let departureSequence: Int
}

struct Status: Codable {
    let version: String
    let health: Int
}

struct TravelData: Codable {
    let departures: [Departure]
    let status: Status

    /// Commented out this as do not know what is in the dictionary!

//    let stops: Stop
//    let routes: Route
//    let runs: Run
//    let directions: Direction
//    let disruptions: Disruption
}

//    struct Stop: Codable {
//    }
//
//    struct Route: Codable {
//    }
//
//    struct Run: Codable {
//    }
//
//    struct Direction: Codable {
//    }
//
//    struct Disruption: Codable {
//    }

Paul has done a nice extension that you can use set up a new file called Extension-URL and add this

/// A URLSession extension that fetches data from a URL and decodes to some Decodable type.
/// Usage: let user = try await URLSession.shared.decode(UserData.self, from: someURL)
/// Note: this requires Swift 5.5.
extension URLSession {
    func decode<T: Decodable>(
        _ type: T.Type = T.self,
        from url: URL,
        keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys,
        dataDecodingStrategy: JSONDecoder.DataDecodingStrategy = .deferredToData,
        dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate
    ) async throws  -> T {
        let (data, _) = try await data(from: url)

        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = keyDecodingStrategy
        decoder.dataDecodingStrategy = dataDecodingStrategy
        decoder.dateDecodingStrategy = dateDecodingStrategy

        let decoded = try decoder.decode(T.self, from: data)
        return decoded
    }
}

And then in the View

struct ContentView: View {
    @State private var departures = [Departure]()

    var body: some View {
        VStack {
            ForEach(departures, id: \.runId) { departure in
                Text(departure.platformNumber)
            }
        }
        .padding()
        .task { await fetch() }
    }

    /// Call for get JSON data from URL
    /// requires `@State private var name = [Decodable]()`
    /// and `.task { await fetch() }`
    func fetch() async {
        do  {
            let url = URL(string: "https://blahblah.json")!
            async let travelData = try await URLSession.shared.decode(TravelData.self, from: url,
                                                                      keyDecodingStrategy: .convertFromSnakeCase,
                                                                      dateDecodingStrategy: .iso8601)
            departures = try await travelData.departures
        } catch {
            print("Failed to fetch data!")
        }
    }
}

2      

Hi @NigelGee

Thanks for that additional context, also Ducky Model Editor is ace - Cheers again

1      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.