0

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"
}
5
  • 1
    I would suggest not making everything optional and use CodingKey enums so you can use better property names. As for the decoding issue you need to implement a custom init(from:), I would first try to decode the list as an array of UserInfo and if that fails just assign an empty array to the property Commented Sep 18, 2021 at 7:43
  • I just saw that your json isn't valid, please post correct json. Commented Sep 18, 2021 at 20:55
  • @JoakimDanielson Thanks for your comment? which case of JSON isn't valid? And Do you mind showing me codes decoding the list as an array of Userinfo? Commented Sep 20, 2021 at 14:37
  • Actually the issue is that all examples are missing a closing } (I must have made a mistake earlier because I thought there were more issues with them) Commented Sep 20, 2021 at 14:41
  • @JoakimDanielson you were right. I was missing the closing } but the problem still remains the same. Commented Sep 20, 2021 at 14:52

2 Answers 2

1

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)
  }
}
Sign up to request clarification or add additional context in comments.

Comments

0

You can write this code to resolve this array string issue.

struct UserDataResponse: Codable {
let RESPONSE: UserData?
}

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

enum CustomMetadataType: Codable {
    case array([String])
    case string(String)
init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    do {
        self = try .array(container.decode(Array.self))
    } catch DecodingError.typeMismatch {
        do {
            self = try .string(container.decode(String.self))
        } catch DecodingError.typeMismatch {
            throw DecodingError.typeMismatch(CustomMetadataType.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Encoded payload not of an expected type"))
        }
    }
}

func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()
    switch self {
    case .array(let array):
        try container.encode(array)
    case .string(let string):
        try container.encode(string)
    }
    }
}

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

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.