iOS 개발일기

[Swift] JSONParser - Decodable(2) 본문

iOS/Swift

[Swift] JSONParser - Decodable(2)

맨날 까먹으니 적어두자 2022. 3. 15. 21:55

전편

 

[Swift] JSONParser - Decodable(1)

Swift에서 JSON 파싱을 위한 방법에는 JSONSerialization Decodable 이 두가지 방법이 있습니다. 오늘은 Decodable을 이용하여 JSON 파싱을 하는 방법에 대해서 알아보겠습니다. 먼저 파싱에 필요한 예제 데이

93bpm.tistory.com

 

이번 편에서는 Decodable에 대한 이해도를 조금 더 높힐 수 있는 시간을 가져보겠습니다.

 

Decodable은 사용하는 방법이 편하지만 생각보다 제약이 많기 때문에 정확하게 이해하고 사용해야합니다.

간단한 형식이라면 문제없이 사용할 수 있겠지만

 

언제나 간단한 파싱만 하지않는 법이죠...허허

 

어떠한 상황에서 에러가 나는지 어떻게 대처해야되는지 몇 가지 알아보도록 하겠습니다.

 

먼저, 기본 모델을 하나 만들어주겠습니다.

Struct User Model
struct User {

    private(set) var uid: Int
    private(set) var name: String
    private(set) var age: Int
}

extension User: Decodable {

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        self.uid = try container.decode(Int.self, forKey: .uid)
        self.name = try container.decode(String.self, forKey: .name)
        self.age = try container.decode(Int.self, forKey: .age)
    }
    
    private enum CodingKeys: String, CodingKey, CaseIterable {
        case uid = "user_id",
             name = "user_name",
             age = "user_age"
    }
}

이 모델을 이용하여 Decode를 진행해볼텐데요.

 

Decode를 진행하면서 받아오는 JSON Data의 형식이 Array인지 Dictionary인지를 먼저 파악해주어야 합니다.

JSON Data의 자료형에 따라서도 decode의 방법이 달라지게 되거든요.

 

JSON Data의 자료형이 Array일 경우에는 decode를 Dictionary로 할 경우에는 에러가 발생하게 됩니다.

반대로 Dictionary일 경우에 decode를 Array로 할 경우에도 당연히 에러가 발생하겠죠?

(당연한 이야기이겠지만 data에 자료가 하나만 들어있냐 여러 개가 들어있냐 차이에 따라 형태를 잘 지정해주셔야합나다.)

 

예를 들어보겠습니다.

JSON Data가 한 가지만 존재할 경우
let jsonString = """
                 {
                     "user_id"   : 0,
                     "user_name" : "hoon",
                     "user_age"  : 30
                 }
                 """
                 
func decode() {

    //success, data에 하나의 자료만 들어있기 때문에 Dictionary로 decode를 진행합니다.
    if let data = jsonString.data(using: .utf8) {
        do {
            if let user = try JSONDecodable().decode(User.self, from: data) {
                print(user.id)   //0
                print(user.name) //"hoon"
                print(user.age)  //30
            }
            
        } catch {
            print("parse error:", error)
        }
    }
    
    //fail, data는 하나인데 Array로 decode를 할 경우 에러가 발생합니다.
    if let data = jsonString.data(using: .utf8) {
        do {
            if let user = try JSONDecoder().decode([User].self, from: data) {
                print(user.id)
                print(user.name)
                print(user.age)
            }
            
        } catch {
            print("parse error:", error) 
        }
    }
}
jsonString error: typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil))

이렇게 data는 하나인데 Array로 decode를 하려고하면 에러가 발생하게 됩니다.

 

이 밑은 반대의 경우인데 접어놓겠습니다.

더보기
JSON Data가 여러 가지가 존재할 경우
                 
let jsonString = """
                 [
                     {
                         "user_id"   : 1,
                         "user_name" : "hoon",
                         "user_age"  : 28
                     },
                     {
                         "user_id"   : 2,
                         "user_name" : "joon",
                         "user_age"  : 30
                     },
                     {
                         "user_id"   : 3,
                         "user_name" : "min",
                         "user_age"  : 32
                     }
                 ]
                 """
                 
func decode() {

    //fail, 여러 개의 data가 존재하지만 하나로 decode하려는 경우 에러가 발생합니다.
    if let data = jsonString.data(using: .utf8) {
        do {
            if let users = try JSONDecoder().decode(User.self, from: data) {
                users.forEach { user in 
                    print(user.name)
                }
            }

        } catch {
            print("parse error:", error)
        }
    }
    
    //success
    if let data = jsonString.data(using: .utf8) {
        do {
            if let users = try JSONDecoder().decode([User].self, from: data) {
                users.forEach { user in 
                    print(user.name) //"hoon", "joon", "min"
                }
            }        
            
        } catch {
            print("parse error:", error)
        } 
    }
}
jsonString error: typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil))

Decodable Model을 만들기 전에 먼저 parsing의 대상이 되는 data에 대한 형태를 먼저 파악하시면 도움이 되실 것 같네요.

 

다음으로 넘어가겠습니다.

드물게 일어나는 일이긴 합니다만 data의 형태가 변하는 것인데요.

 

예를 들어, 

  • 'user_id'가 null을 허용하거나
  • 'user_id'가 삭제되었다면

기존에 User Model을 이용해서 decode하게 된다면 어떻게 될까요?

{
    "user_id"   : 0,
    "user_name" : "hoon",
    "user_age"  : 30
}

//"user_id"에 null 허용
{
    "user_id"   : null,
    "user_name" : "hoon",
    "user_age"  : 30
}

또는

//"user_id" 삭제
{
    "user_name" : "hoon",
    "user_age"  : 30
}

null 혹은 프로퍼티가 없을 경우에는 에러가 발생하게 됩니다.

 

alueNotFound(Swift.Int, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "user_id", intValue: nil)], debugDescription: "Expected Int value but found null instead.", underlyingError: nil))

위와 같이 값을 찾을 수 없다고 나오게되죠.

 

왜 이럴까요?

 

self.uid = try container.decode(Int.self, forKey: .uid)

여기에 decode 함수가 원인인데요.

보시면 반환형태가 Optional이 아니기 때문에 에러가 발생하게 되죠.

 

그러면 어떻게 해결할 수 있을까요?

기본값을 설정하거나 Optional을 허용할 수 있도록 해주는 방법 정도가 있는 것 같아요. 

방법 1. 기본 값 설정
struct User {
    private(set) var uid: Int
    ...
}

extension User {
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        //set default value
        self.uid = (try? container.decode(Int.self, forKey: .uid)) ?? 999 
        ...
    }
    
    private enum CodingKeys: String, CodingKey, CaseIterable { ... }
}

 

try를 Optional처리를 해주어서 기본 값을 설정 해주는 방법이 있습니다.

기본 값이 꼭 필요한 변수에 적용하기에는 좋은 방법인 것 같습니다.

 

방법 2. Optional 허용

저는 개인적으로 이 방법을 자주 쓰는 것 같네요.

Optional을 허용하게되면 Model을 사용할 때 예외처리를 하기에 용이하기 때문에 자주쓰는 것 같아요.

(너무 많은 Optional을 사용하게되면 바인딩에 번거로움이 있긴하지만요...ㅎㅎ;)

 

decode() 함수는 해당 프로퍼티에 값이 null 또는 존재하지 않는다면 에러가 발생하지만 

decodeIfPresent() 함수는 반환형태가 Optional이므로 null이나 프로퍼티가 존재하지 않더라도 nil처리를 할 수 있죠.

 

변수를 Optional로 변경하여 nil을 가지는 것과

struct User {
    private(set) var uid: Int?
    ...
}

extension User: Decodable {

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyed: CodingKeys.self)
        
        self.uid = try container.decodeIfPresent(Int.self, forKey: .uid)
        ...
    }
    
    private enum CodingKeys: String, CodingKey, CaseIterable { ... }
}

decodeIfPresent()를 이용하여 방법 1. 처럼 기본 값을 설정해줄 수도 있어요.

self.uid = (try container.decodeIfPresent(Int.self, forKey: .uid)) ?? 999

 

 

 

오늘은 에러가 발생하는 상황들과 대처방법을 알아보았습니다.

저도 공부를 하면서 쓰는거라 부족한 점이 많은지라 지적은 언제나 감사하게 받겠습니다.