2

Considering the following gist: https://gist.github.com/anonymous/0703a591f97fa9f6ad35b29234805fbd - no efficient way to display all of the relevant information as the files are of considerable length. Apologies

I have some nested JSON objects which I'm trying to decode.

{  
   "establishments":[  
      { ... }
   ],
   "meta":{ ... },
   "links":[  

   ]
}

^ The establishments array contains Establishment objects

My code runs successfully up until line 7 of ViewController.swift where I'm actually utilising Swift 4's JSONDecode() functionality.

ViewController.swift

let jsonURLString = "http://api.ratings.food.gov.uk/Establishments?address=\(self.getPostalCode(place: place))&latitude=\(place.coordinate.latitude.description)&longitude=\(place.coordinate.longitude.description)&maxDistanceLimit=0&name=\(truncatedEstablishmentName[0].replacingOccurrences(of: "'", with: ""))"

guard let url = URL(string: jsonURLString) else { print("URL is invalid"); return }

URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
    guard let data = data, error == nil, response != nil else {
        print("Something went wrong")
        return
    }
    //print("test")
    do {
        let establishments = try JSONDecoder().decode(Establishments.self, from: data)
        print(establishments)
    } catch {
        print("summots wrong")
    }

}).resume()

The output I get for the mapped JSON object seen in Establishment.swift is: summots wrong - on line 11 of ViewController.swift.

I believe the reason it is not working as expected is because the JSON format is like so:

{  
   "establishments":[ ... ],
   "meta":{ ... },
   "links":[  ]
}

but I don't know if my class structure/ what I'm doing in my view controller satisfies this layout criteria.

Can anyone spot where I'm going wrong? I think it's something very basic but I can't seem to get my head around it

I also understand that I'm not supposed to name attributes of classes with a capital letter - apologies for breaking any conventions

EDIT: I printed the error instead of my own statement and it is as follows:

instead of: print("summots wrong")

I wrote: print(error)

dataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: Optional(Error Domain=NSCocoaErrorDomain Code=3840 "JSON text did not start with array or object and option to allow fragments not set." UserInfo={NSDebugDescription=JSON text did not start with array or object and option to allow fragments not set.})))

As Hamish correctly pointed out, I'm not receiving correct JSON data from the GET request. In fact, I'm not receiving any JSON at all. The API I'm using defaults to XML format when the x-api-version is not set to 2.

Instead of //print("test") in my ViewController.swift file, I used the recommended: print(String(data: data, encoding: .utf8)) line

I used Postman with the following headers: x-api-version: 2, accept: application/json and content-type: application/json to retrieve my JSON output. If I use the same GET string (with all of the parameters filled out properly) in my browser window, I will receive an error stating: The API 'Establishments' doesn't exist in an XML format.

Is there any way for me to send a header with this URL?

EDIT 2: I have changed the request to use URLRequest instead of just URL.

Here is my updated ViewController.swift code:

let jsonURLString = "http://api.ratings.food.gov.uk/Establishments?address=\(self.getPostalCode(place: place))&latitude=\(place.coordinate.latitude.description)&longitude=\(place.coordinate.longitude.description)&maxDistanceLimit=0&name=\(truncatedEstablishmentName[0].replacingOccurrences(of: "'", with: ""))"

guard let url = URL(string: jsonURLString) else { print("URL is invalid"); return }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.addValue("x-api-version", forHTTPHeaderField: "2")
request.addValue("accept", forHTTPHeaderField: "application/json")
request.addValue("content-type", forHTTPHeaderField: "application/json")

URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
   guard let data = data, error == nil, response != nil else {
       print("Something went wrong")
       return
   }
   print(String(data: data, encoding: .utf8))
// do {
//     let establishments = try JSONDecoder().decode(Establishments.self, from: data)
//     print(establishments)
// } catch {
//     print(error)
// }

}).resume()

I'm getting the error: HTTP Error 400. The request has an invalid header name

I did some checks, it the request just ignores the x-api-version header value of 2.

Any ideas?

EDIT 3: The correct format for addValue should have been: request.addValue("2", forHTTPHeaderField: 'x-api-version and so on.

I am now faced with an issue with my class declaration, specifically in my 'meta' class.

Meta.(CodingKeys in _DF4B170746CD5543281B14E0B0E7F6FB).dataSource], debugDescription: "Expected String value but found null instead.", underlyingError: nil

It's complaining because my class declaration is not matching the data it's expecting from the JSON object.

In my JSON response (refer to the gist) there are two meta objects, one contains the value of null for dataSource and the other being Lucene.

Is there any way for me to accept 'null' values as well as strings in my class initialisers, all while keeping in compliance with the Decodable protocol?

That section of the JSON response is dead to me, do I still need to create objects for it or can I get away with just ignoring them? - without declaring anything. Or do I specifically need to note down everything in the JSON response to be able to use the data?

11
  • Also please copy and paste the relevant parts of your code (preferably a minimal reproducible example) into the question body itself rather than linking to a gist; we shouldn't be expected to leave this page to understand your question. Commented Oct 24, 2017 at 16:19
  • Thanks for the constructive criticism guys, I've made some edits to the question Commented Oct 24, 2017 at 16:28
  • 1
    Okay, so the error is saying your JSON doesn't start with a { or [ – try printing out String(data: data, encoding: .utf8) and seeing what you get (it may not even be valid UTF8). Commented Oct 24, 2017 at 16:31
  • 1
    Yes, it is possible to set headers but you need to use URLRequest instead of URL to create your dataTask Commented Oct 24, 2017 at 16:54
  • 1
    It seems that you inverted the header value and the header name in your 3 calls to request.addValue Commented Oct 24, 2017 at 17:53

2 Answers 2

1

To decode automatically, your model class properties must be matched with response value types. From JSON response, I can see few data model properties are not matched with JSON value type. For example:

//inside response
"Hygiene": 10, // which is integer type

// properties in Scores
let Hygiene: String

Either you have to change model class properties as response or JSON response as your class properties.

For demonstrating, I've modified your model class with following changes:

        // changed from String
        let Hygiene: Int 
        let Structural: Int
        let ConfidenceInManagement: Int

        // changed from Double
        let longitude: String
        let latitude: String

        // changed to optional
        let dataSource: String?
        let returncode: String?

        // changed from Int
        let RatingValue: String

Replace your model classes with below changes:

class Scores: Decodable {
        // set as string because we can expect values such as 'exempt'
        let Hygiene: Int
        let Structural: Int
        let ConfidenceInManagement: Int

        init(Hygiene: Int, Structural: Int, ConfidenceInManagement: Int) {
            self.Hygiene = Hygiene
            self.Structural = Structural
            self.ConfidenceInManagement = ConfidenceInManagement
        }
    }

    class Geocode: Decodable {
        let longitude: String
        let latitude: String

        init(longitude: String, latitude: String) {
            self.longitude = longitude
            self.latitude = latitude
        }
    }

    class Meta: Decodable {
        let dataSource: String?
        let extractDate: String
        let itemCount: Int
        let returncode: String?
        let totalCount: Int
        let totalPages: Int
        let pageSize: Int
        let pageNumber: Int

        init(dataSource: String, extractDate: String, itemCount: Int, returncode: String, totalCount: Int, totalPages: Int, pageSize: Int, pageNumber: Int) {

            self.dataSource = dataSource
            self.extractDate = extractDate
            self.itemCount = itemCount
            self.returncode = returncode
            self.totalCount = totalCount
            self.totalPages = totalPages
            self.pageSize = pageSize
            self.pageNumber = pageNumber
        }
    }

    class Establishments: Decodable {
        let establishments: [Establishment]

        init(establishments: [Establishment]) {
            self.establishments = establishments
        }
    }

    class Establishment: Decodable {
        let FHRSID: Int
        let LocalAuthorityBusinessID: String
        let BusinessName: String
        let BusinessType: String
        let BusinessTypeID: Int
        let AddressLine1: String
        let AddressLine2: String
        let AddressLine3: String
        let AddressLine4: String
        let PostCode: String
        let Phone: String
        let RatingValue: String
        let RatingKey: String
        let RatingDate: String
        let LocalAuthorityCode: Int
        let LocalAuthorityName: String
        let LocalAuthorityWebSite: String
        let LocalAuthorityEmailAddress: String
        let scores: Scores
        let SchemeType: String
        let geocode: Geocode
        let RightToReply: String
        let Distance: Double
        let NewRatingPending: Bool
        let meta: Meta
        let links: [String]

        init(FHRSID: Int, LocalAuthorityBusinessID: String, BusinessName: String, BusinessType: String, BusinessTypeID: Int, AddressLine1: String, AddressLine2: String, AddressLine3: String, AddressLine4: String, PostCode: String, Phone: String, RatingValue: String, RatingKey: String, RatingDate: String, LocalAuthorityCode: Int, LocalAuthorityName: String, LocalAuthorityWebSite: String, LocalAuthorityEmailAddress: String, scores: Scores, SchemeType: String, geocode: Geocode, RightToReply: String, Distance: Double, NewRatingPending: Bool, meta: Meta, links: [String]) {

            self.FHRSID = FHRSID
            self.LocalAuthorityBusinessID = LocalAuthorityBusinessID
            self.BusinessName = BusinessName
            self.BusinessType = BusinessType
            self.BusinessTypeID = BusinessTypeID
            self.AddressLine1 = AddressLine1
            self.AddressLine2 = AddressLine2
            self.AddressLine3 = AddressLine3
            self.AddressLine4 = AddressLine4
            self.PostCode = PostCode
            self.Phone = Phone
            self.RatingValue = RatingValue
            self.RatingKey = RatingKey
            self.RatingDate = RatingDate
            self.LocalAuthorityCode = LocalAuthorityCode
            self.LocalAuthorityName = LocalAuthorityName
            self.LocalAuthorityWebSite = LocalAuthorityWebSite
            self.LocalAuthorityEmailAddress = LocalAuthorityEmailAddress
            self.scores = scores
            self.SchemeType = SchemeType
            self.geocode = geocode
            self.RightToReply = RightToReply
            self.Distance = Distance
            self.NewRatingPending = NewRatingPending
            self.meta = meta
            self.links = links
        }
    }

Hope it will work.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you, this has helped tremendously. I thought I had tried this before (adding them as optional parameters but obviously not all of the 'null'able values)
1

This code should retrieve data as Json, with the correct content-type set in headers.

let jsonURLString = "http://api.ratings.food.gov.uk/Establishments?address=\(self.getPostalCode(place: place))&latitude=\(place.coordinate.latitude.description)&longitude=\(place.coordinate.longitude.description)&maxDistanceLimit=0&name=\(truncatedEstablishmentName[0].replacingOccurrences(of: "'", with: ""))"

guard let url = URL(string: jsonURLString) else { print("URL is invalid"); return }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.addValue("2", forHTTPHeaderField: "x-api-version")
request.addValue("application/json", forHTTPHeaderField: "accept")
request.addValue("application/json", forHTTPHeaderField: "content-type")

URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
   guard let data = data, error == nil, response != nil else {
       print("Something went wrong")
       return
   }

  do {
     let establishments = try JSONDecoder().decode(Establishments.self, from: data)
     print(establishments)
   } catch {
     print(error)
   }
}).resume()

Then every property that can be null needs to be declared as optional. Considering only data-source can be null, your meta object declaration will be like this:

class Meta: Decodable {
    let dataSource: String?
    let extractDate: String
    let itemCount: Int
    let returncode: String
    let totalCount: Int
    let totalPages: Int
    let pageSize: Int
    let pageNumber: Int
}

As a final note, if you prefer to have lowercased properties everywhere in your model, you can redefine the keys by overriding the CodingKey enum.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.