Search code examples
jsonswiftdecode

JSON decoding fails when response has different types


I have an API response below. The "USER_LIST" response is different based on the value of "DATA_NUM".
The problem I have is when the "DATA_NUM" is "0", it returns an empty string AND when "DATA_NUM" is "1", the "USER_LIST" returns both object and an empty string so that I can't decode with a model below. I want to construct a model that's suitable for every case regardless of the value of the "DATA_NUM".

How can I achieve this? Thanks in advance.

API response

// when "DATA_NUM": "0"
{
  "RESPONSE": {
    "DATA_NUM": "0",
    "USER_LIST": ""
  }
}

// when "DATA_NUM": "1"
{
  "RESPONSE": {
    "DATA_NUM": "1",
    "USER_LIST": [
     {
      "USER_NAME": "Jason",
      "USER_AGE": "30",
      "ID": "12345"
     },
     ""
  ]
 }
}

// when "DATA_NUM": "2"
{
  "RESPONSE": {
    "DATA_NUM": "2",
    "USER_LIST": [
     {
      "USER_NAME": "Jason",
      "USER_AGE": "30",
      "ID": "12345"
     },
     {
     "USER_NAME": "Amy",
      "USER_AGE": "24",
      "ID": "67890"
     }
   ]
 }
}

Model

struct UserDataResponse: Codable {
  let RESPONSE: UserData?
}

struct UserData: Codable {
  let DATA_NUM: String?
  let USER_LIST: [UserInfo]?
}

struct UserInfo: Codable {
  let USER_NAME: String?
  let USER_AGE: String?
  let ID: String?
}

Decode

do {
  let res: UserDataResponse = try JSONDecoder().decode(UserDataResponse.self, from: data)
  guard let userData: UserData = res.RESPONSE else { return }
  print("Successfully decoded", userData)
} catch {
  print("failed to decode") // failed to decode when "DATA_NUM" is "0" or "1"
}

Solution

  • Here is a solution using a custom init(from:) to handle the strange USER_LIST

    struct UserDataResponse: Decodable { let response : UserData

      enum CodingKeys: String, CodingKey {
        case response = "RESPONSE"
      }
    }
    
    struct UserData: Decodable {
      let dataNumber: String
      let users: [UserInfo]
      
      enum CodingKeys: String, CodingKey {
        case dataNumber = "DATA_NUM"
        case users = "USER_LIST"
      }
      
      init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        dataNumber = try container.decode(String.self, forKey: .dataNumber)
        if let _ = try? container.decode(String.self, forKey: .users) {
          users = []
          return
        }
        
        var nestedContainer = try container.nestedUnkeyedContainer(forKey: .users)
        
        var temp: [UserInfo] = []
        do {
          while !nestedContainer.isAtEnd {
            let user = try nestedContainer.decode(UserInfo.self)
            temp.append(user)
          }
        } catch {}
        
        self.users = temp
      }
    }
    
    struct UserInfo: Decodable {
      let name: String
      let age: String
      let id: String
      
      enum CodingKeys: String, CodingKey {
        case name = "USER_NAME"
        case age = "USER_AGE"
        case id = "ID"
      }
    }
    

    An example (data1,data2,data3 corresponds to the json examples posted in the question)

    let decoder = JSONDecoder()
    for data in [data1, data2, data3] {
      do {
        let result = try decoder.decode(UserDataResponse.self, from: data)
        print("Response \(result.response.dataNumber)")
        print(result.response.users)
      } catch {
        print(error)
      }
    }
    

    Output

    Response 0
    []
    Response 1
    [__lldb_expr_93.UserInfo(name: "Jason", age: "30", id: "12345")]
    Response 2
    [__lldb_expr_93.UserInfo(name: "Jason", age: "30", id: "12345"), __lldb_expr_93.UserInfo(name: "Amy", age: "24", id: "67890")]


    Edit with alternative solution for the while loop

    In the above code there is a while loop surrounded by a do/catch so that we exit the loop as soon an error is thrown and this works fine since the problematic empty string is the last element in the json array. This solution was chosen since the iterator for the nestedContainer is not advanced to the next element if the decoding fails so just doing the opposite with the do/catch (where the catch clause is empty) inside the loop would lead to an infinite loop.

    An alternative solution that do work is to decode the "" in the catch to advance the iterator. I am not sure if this is needed here but the solution becomes a bit more flexible in case the empty string is somewhere else in the array than last.

    Alternative loop:

    while !nestedContainer.isAtEnd {
      do {
        let user = try nestedContainer.decode(UserInfo.self)
        temp.append(user)
      } catch {
        _ = try! nestedContainer.decode(String.self)
      }
    }