1

I know this type of question seems to be answered a lot but I really can't seem to make this work. I'm trying to decode some JSON data into my data structs. I think the problem is there. I may have my data model wrong, but can't quite work it out. The data is not an array, there is an array within it. Its trying to decode a dictionary into array but when I try to initialise my results variable as something other than array it won't build. I'll submit my code and the JSON data in the hopes someone can shed light!

The error I'm getting is:

JSON decode failed: Swift.DecodingError.typeMismatch(Swift.Array, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Array but found a dictionary instead.", underlyingError: nil))

thank you so much


import SwiftUI

struct DataFormatted: Codable, Hashable {
    
    var deliveryPoints: [DeliveryPoints]
    var deliveryPointCount: Int
    var postalCounty: String
    var traditionalCounty: String
    var town: String
    var postCode: String
}

struct DeliveryPoints: Codable, Hashable {
    var organisationName: String
    var departmentName: String
    var line1: String
    var line2: String
    var udprn: String
    var dps: String
    
}

struct ContentView: View {

// I reckon the error is here:
    @State private var results = [DataFormatted]()

    var body: some View {
        VStack{
            List{
                ForEach(results, id: \.self) { result in
                    Text(result.postalCounty)
                }
                
            }
        }
        .task {
            await loadData()
        }
    }
    
    func loadData() async {
        
       
        guard let url = URL(string: "https://pcls1.craftyclicks.co.uk/json/rapidaddress?key=APIKEY&postcode=aa11aa&response=data_formatted") else {
            print("Invalid URL")
            return
        }
        
        var request = URLRequest(url: url)
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpMethod = "GET"
        
        do {
            let (data, _) = try await URLSession.shared.data(for: request)
            let decoder = JSONDecoder()
            decoder.keyDecodingStrategy = .convertFromSnakeCase
            
            let decodedResponse = try decoder.decode([DataFormatted].self, from: data)
            
            results = decodedResponse
            
        } catch let jsonError as NSError {
            print("JSON decode failed: \(jsonError)")
          }
    }
}

JSON Data:

{
    "delivery_points":[
        {
            "organisation_name":"THE BAKERY",
            "department_name":"",
            "line_1":"1 HIGH STREET",
            "line_2":"CRAFTY VALLEY",
            "udprn":"12345678",
            "dps":"1A"
        },
        {
            "organisation_name":"FILMS R US",
            "department_name":"",
            "line_1":"3 HIGH STREET",
            "line_2":"CRAFTY VALLEY",
            "udprn":"12345679",
            "dps":"1B"
        }
    ],
    "delivery_point_count":2,
    "postal_county":"POSTAL COUNTY",
    "traditional_county":"TRADITIONAL COUNTY",
    "town":"BIG CITY",
    "postcode":"AA1 1AA"
}
  • Replace `jsonError.localizedDescription` with `jsonError` to get the real error. But it's pretty clear: The root object is **not** an array and where is `line3` in the JSON? – vadian Oct 07 '22 at 06:52
  • agreed. I've updated my question. its trying to decode a dictionary into an array. but when I try to change my code to decode into an array I can't initialise my results variable into an array of DataFormatted. it won't build.... can you help? – Layth Tameem Oct 07 '22 at 07:10
  • You have to decode `DataFormatted.self` rather than `[DataFormatted].self`. By the way: `postcode` is not affected by the snake case conversion. The struct member name must match the key – vadian Oct 07 '22 at 07:12
  • So I think I've sorted this out however I've got @State private var results: DataFormatted in my code just before my var body - the aim being to create an instance of my struct called results right?! . All the code seems to check out but I can't build it as in the main app file it says : 'ContentView' initializer is inaccessible due to 'private' protection level I think it wants me to set an initial value for results - but I obviously can't - how do I get around this?!! – Layth Tameem Oct 07 '22 at 13:33
  • A good practice in SwiftUI is a non-optional `state` property, an enum with associated values. For example for asynchronous data reasonable states are `idle` – initial state, does nothing, `loading(Double)` – to show a progress view with a percent value, `loaded(DataFormatted)` – the main view containing the received data and `failed(Error)` – to show an error view. – vadian Oct 07 '22 at 15:33
  • So I've tried doing this: `struct DataFormatted: Codable { var deliveryPoints: [DeliveryPoint]? var deliveryPointCount: Int = 0 var postalCounty: String = "" var traditionalCounty: String = "" var town: String = "" var postcode: String = "" // <-- postcode } struct DeliveryPoint: Codable { var organisationName: String var departmentName: String var line1: String? var line2: String? var line3: String? var udprn: String var dps: String }` but it doesn't help... – Layth Tameem Oct 07 '22 at 17:26
  • fixed it - I changed my @main file to have: ContentView(results: DataFormatted()) That made it compile – Layth Tameem Oct 07 '22 at 17:51

1 Answers1

1

try something like this:

struct DataFormatted: Codable {
    var deliveryPoints: [DeliveryPoint]
    var deliveryPointCount: Int
    var postalCounty: String
    var traditionalCounty: String
    var town: String
    var postcode: String // <-- postcode
}

struct DeliveryPoint: Codable {
    var organisationName: String
    var departmentName: String
    var line1: String?
    var line2: String?
    var line3: String?
    var udprn: String
    var dps: String
}
  

and use it like this:

   let apiResponse = try decoder.decode(DataFormatted.self, from: data)

EDIT-1: here is the code I used for testing:

// -- here, default values for convenience
struct DataFormatted: Codable {
    var deliveryPoints: [DeliveryPoint] = []
    var deliveryPointCount: Int = 0
    var postalCounty: String = ""
    var traditionalCounty: String = ""
    var town: String = ""
    var postcode: String = ""  // <-- postcode
}

struct DeliveryPoint: Hashable, Codable {  // <-- here
    var organisationName: String
    var departmentName: String
    var line1: String?
    var line2: String?
    var line3: String?
    var udprn: String
    var dps: String
}

struct ContentView: View {
    @State private var results = DataFormatted()
    
    var body: some View {
        VStack{
            Text(results.postalCounty)
            Text(results.town)
            List {
                ForEach(results.deliveryPoints, id: \.self) { point in
                    Text(point.organisationName)
                }
            }
        }
        .task {
            await loadData()
        }
    }
    
    func loadData() async {
        let apikey = "your-key"  // <-- here
        guard let url = URL(string: "https://pcls1.craftyclicks.co.uk/json/rapidaddress?key=\(apikey)&postcode=aa11aa&response=data_formatted") else {
            print("Invalid URL")
            return
        }
        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            let decoder = JSONDecoder()
            decoder.keyDecodingStrategy = .convertFromSnakeCase
            results = try decoder.decode(DataFormatted.self, from: data)  // <-- here
        } catch {
            print("JSON decode failed: \(error)")
        }
    }
}
  • Thank you. with this in place, what do I use for my @State variable? if I do ' State private var results: DataFormatted' it doesn't like it. Cos I need a way to get the apiResponse out of the do/catch block don't I? Do you know what I mean? – Layth Tameem Oct 07 '22 at 12:35
  • So I've got @State private var results: DataFormatted in my code just before my var body. All the code seems to check out but I can't build it as in the main app file it says : 'ContentView' initializer is inaccessible due to 'private' protection level I think it wants me to set an initial value for results - but I obviously can't - how do I get around this?!! – Layth Tameem Oct 07 '22 at 13:32
  • see my updated answer. Note, **do not** show your secret api key (in the url), edit your post and remove it. – workingdog support Ukraine Oct 07 '22 at 23:44