Swift Decodable, is it possible to use JSONDecoder to parse JSON like this into my target object?

102 views Asked by At

I have a WebAPI which returns JSON like the following: {"result":"true","message":"2023-07-01 16:09:02"}

I have a Decodable object defined like this:

struct ActionClock: Codable{
    var result: Bool? = false
    var message: String? = "invalid"
}

I have code that looks like the following and fails when attempting to decode the JSON.

NOTE: This is from a code sample so some of it I am unfamiliar with..

func loadTime() {
     guard let url = URL(string: "https://actionmobile.app/GetTime" ) 
     else {
            print("Invalid URL")
            return
       }
 let request = URLRequest(url: url)

 URLSession.shared.dataTask(with: request) { data, response, error in
  if let data = data {
     print("data: \(data) \(Date())")
     if let response = try? JSONDecoder().decode(ActionClock.self, from: data) {
      print("is this called?")
      DispatchQueue.main.async {
         self.atime = response
         print("in LOADTIME")
       }
      print("response: \(response)")
      return
    }
  }
  else{
     print("I failed")
    }
 }.resume() 
}

When this code runs, it prints the number of bytes in data and the date (successfully hits the webApi and retrieves the json), but the JSONDecoder.decode() call fails to parse my JSON.

Can you tell me:

  1. Is it possible to use the JSONDecoder to decode this particular JSON?
  2. Do I need to change something in my Codable class so it will parse correctly?
  3. Can you show me how to wrap the decode() call in a proper try...catch so I can see the exception that is thrown?
4

There are 4 answers

2
raddevus On BEST ANSWER

I altered the code to print the error (implemented do { try } ) and discovered that the JSONDecoder doesn't think it can parse the Boolean value returned in the original JSON because it believes it is a string (because the value is wrapped in double-quotes).

Here's the error:

typeMismatch(Swift.Bool, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "result", intValue: nil)], debugDescription: "Expected to decode Bool but found a string/data instead.", underlyingError: nil))

I think that is incorrect really, because if you run JSONLint on my original JSON you will see the following:

jsonlint valid json

I changed the type in my original Codable object and everything worked as expected. I also made them non-nullable since I init the values.

struct ActionClock: Codable{
    var result: String = "false"
    var message: String = "invalid"
}

Here's the updated code:

func loadTime() {
    guard let url = URL(string: "https://actionmobile.app/GetTime" ) else {
        print("Invalid URL")
        return
    }
    let request = URLRequest(url: url)

    URLSession.shared.dataTask(with: request) { data, response, error in
        if let data = data {
            print("data: \(data) \(Date())")
            do {
                let response = try JSONDecoder().decode(ActionClock.self, from: data)
                print("is this called?")
                DispatchQueue.main.async {
                    self.atime = response
                    print("in LOADTIME")
                }
                print("response: \(response)")
                return
                
            }catch {
                print ("\(error)")
            }
        }
    }.resume()
}
0
Ptit Xav On

Here is how you test for error and json error :

func loadTime() {
        guard let url = URL(string: "https://actionmobile.app/GetTime" )
        else {
            print("Invalid URL")
            return
        }
        let request = URLRequest(url: url)
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            // always good to test for error
            if let error {
                print("Error:\(error)")
                print("Error:\(error.localizedDescription)")
                return
            }
            if let data = data {
                print("data: \(data) \(Date())")
                // Do/Catch bloc
                do {
                    let response = try JSONDecoder().decode(ActionClock.self, from: data)
                    DispatchQueue.main.async {
                        self.atime = response
                        print("in LOADTIME")
                    }
                    print("is this called?")
                    print("response: \(response)")
                } catch {
                    print("error: \(error)") // note that for JSON decoding error, using localised description will remove the useful part of the error
                }
            }
            else{
                print("I failed")
            }
        }.resume()
    }
1
Krishnapalsinh Gohil On

The Codable struct must be matching the types of the response in order to work correctly.

Server response: {"result":"true","message":"2023-07-01 16:09:02"}

struct ActionClock: Codable {
    let result: String
    let message: String
}

The result property in your struct is expecting a Bool but is actually an String, thus it failed to decode.

If you want to decode it as Bool, even though you get a String via server. You should write your own custom initializer or add getter property to convert from result string to result bool.

Read more on custom initializer here:

1
iOSCaiNiao On

You can try this library ObjMapper, which will automatically convert the string "true" to bool, and the model creation is as follows:

struct ActionClock: Codable{
    @Backed var result: Bool?
    @Backed var message: String?
}

func loadTime() {
    guard let url = URL(string: "https://actionmobile.app/GetTime" ) else {
        print("Invalid URL")
        return
    }
    let request = URLRequest(url: url)

    URLSession.shared.dataTask(with: request) { data, response, error in
        if let data = data {
            print("data: \(data) \(Date())")
 
            let response = ActionClock.decodeJSON(from: data)
            print("is this called?")
            DispatchQueue.main.async {
                self.atime = response
                print("in LOADTIME")
            }
            print("response: \(response)")
            return
        }
    }.resume()
}