티스토리 뷰

iOS

iOS ) Decodable

Zedd0202 2018. 8. 29. 17:32
반응형

안녕하세요 :) Zedd입니다.

Codable을 조금조금씩 써보고 있는 중입니다 XD..

하면서 겪는 그냥 저냥 사실들을.....써보려고 합니다...ㅎㅎ


일단 예제 모델을 하나 만들어봤어요. 

지금부터 옵셔널가지고 실험을 여러가지 해볼건데!!! ?을 잘 보시길 바랍니다.


일단 머 이렇게 생긴 json있다고 치겠음




class Contents: Decodable {

    var contents: [Zedd]

}


class Zedd: Decodable {

    var name: String

    var age: Int

}


그래서 위처럼 만들어줬습니다. Decodable을 준수하면 class && init없음 && 프로퍼티에 기본값 없음이어도 컴파일 에러를 일으키지 않습니다.

Decodable이라고 말했지만...Codable(즉, Encodable & Decodable)을 준수해도 똑같음.

일단 jsonString을 data화 하는데 데이터로 반드시 변한다는 가정을 가지고 강제 언래핑을 할게요.


let jsonString = """

{

    "contents" : null


}

""".data(using: .utf8)!



1번 상황. contents에 null이 오는 경우입니다.


let zedd = try! JSONDecoder().decode(Contents.self, from: self.jsonString)


일부로 에러를 보려고 try!를 했어요. 암튼  저 contents가 null인 경우에는 여기서 에러를 뿜게 됩니다.

왜냐면 Contents클래스의 contents프로퍼티는  nil을 담을 수 없는 [Zedd]타입인데 null, 즉 nil이 왔기 때문입니다 ^_ㅠ...

해결 방법은 



class Contents: Decodable {

    var contents: [Zedd]?

}

class Zedd: Decodable {

    var name: String

    var age: Int

}



이렇게 만들면? 


try!구문도 다행이 넘어가는 것을 볼 수 있습니다. 

내가 지금 너무 쉬운걸 하고있나..? 

물론 값들을 찍어보면 모두 nil

암튼..계속하겠음


2번상황. 


    let jsonString = """

{

    "contents" : [

        {

            "name": null,

             "age": 10


        },

        {

            "name": "alan",

            "age": 20    

        }

    ]

  

}

""".data(using: .utf8)!


name중 하나가 null이 오는 상황입니다.

하지만 우리의 모델은...


class Zedd: Decodable {

    var name: String

    var age: Int

}


이렇게 생겼었죠....? 역시나 옵셔널이 아니기 때문에 1번상황과 마찬가지로 try!에서 에러가 나게 됩니다. 

그럼 뭐다? 옵셔널 ㅇㅇ


class Zedd: Decodable {

    var name: String?

    var age: Int

}


let zedd = try! JSONDecoder().decode(Contents.self, from: self.jsonString)

print(zedd.contents?[0].name, zedd.contents?[0].age) // nil Optional(10)

print(zedd.contents?[1].name, zedd.contents?[1].age) // Optional("alan") Optional(20)


이렇게 하면 첫번째 name은 null이었으니 nil이 담기게 되고, 다음은 alan으로 잘 들어왔으니 다음 인덱스에는 alan이 잘 담기게 됩니다.


그럼 결론은 뭐겠음

확실하게 값 들어온다고 가정할 수 있을때만...........옵셔널이 아닌 타입으로...ㄱ

옵셔널로 처리하면 좋은게


3번상황. 해당 키가 들어오는지 확실하게 알지 못할때 



    let jsonString = """

{

    "contents" : [

        {

     

             "age": 10


        },

        {

            "name": "alan",

            "age": 20      

        }

    ]


}

""".data(using: .utf8)!


자 위에서 배열의 첫번째를 보면 name이 안들어오네요...^_ㅠ;;;

이런상태인데 만약 Zedd class에서 name이 옵셔널이 아니다? 근데 try!를 했어;



그럼 이런 에러를 보실 수 있는데요, key가 없ㄱ다는 에러입니다.

근데 name을 옵셔널로 해주면??


let zedd = try! JSONDecoder().decode(Contents.self, from: self.jsonString)

print(zedd.contents?[0].name, zedd.contents?[0].age) // nil Optional(10)

print(zedd.contents?[1].name, zedd.contents?[1].age) // Optional("alan") Optional(20)


역시나 잘 된다 ^^;


4번째 상황?...이건 뭐 실수일수도 있는데

Json에서는 Int로 들어오는데 내 클래스? 모델에서는 Int타입이 아닐때.


class Zedd: Decodable {

    var name: String?

    var age: String?

}


이렇게 하면.....이름은 잘 들어오고 age만 nil로 들어와야 할 것 같지만 



전부 nil이 들어오게 됩니다. 왜냐면 위 try에서 실패를 하기 때문...만약 위 코드처럼 try? 안하고 try!라고 하면



이런 typeMismatch에러가 나게 됩니다.

나는..................이름이라도..........들어오게 하고싶은........................

ㅇㅋ


class Zedd: Decodable {

    var name: String?

    var age: String?


    private enum CodingKeys: String, CodingKey {

        case name

        case age

    }


    required init(from decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.name = try? container.decode(String.self, forKey: .name)

        self.age = try? container.decode(String.self, forKey: .age)

    }

}


그럼 위 코드는 한번씩 보셨을 텐데요, 굳이 안해줘도 되지만 나만의 디코딩(또는 인코딩) 로직을 추가하고 싶을 때 이렇게 직접 디코딩을 해줄 수 있습니다.

age가 String으로 되어있죠? json에서는 Int인데말이죠.

하지만 이렇게 직접 디코딩하는 코드를 넣어주게 되면 신기하게도...




try에서 에러가 안나고 일단 name은 파싱하게 됩니다.

자, 직접 디코딩 해주는 코드가 나왔으니 또 여러 상황들을 봅시다.

위에서 name이 없었던 상황 기억하시나요?


    let jsonString = """

{

    "contents" : [

        {

     

             "age": 10


        },

        {

            "name": "alan",

            "age": 20      

        }

    ]


}

""".data(using: .utf8)!


여기서 


class Zedd: Decodable {

    var name: String?

    var age: Int?


    private enum CodingKeys: String, CodingKey {

        case name

        case age

    }


    required init(from decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.name = try? container.decode(String.self, forKey: .name)

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

    }

}



이렇게 하고, 



let zedd = try! JSONDecoder().decode(Contents.self, from: self.jsonString)

print(zedd.contents?[0].name, zedd.contents?[0].age)

print(zedd.contents?[1].name, zedd.contents?[1].age)


//nil Optional(10)

//Optional("alan") Optional(20)


하면 name이 nil이 나왔었죠. 


왜 nil이 나왔을까요?? 



required init(from decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.name = try? container.decode(String.self, forKey: .name)

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

    }


바로 try?이기 때문이죠. 


여기서 저 ?만 빼도, 


   let zedd = try! JSONDecoder().decode(Contents.self, from: self.jsonString)


여기서 keyNotFound에러가 나게 됩니다. 


그럼 try로 하고싶긴 한데, 즉 


self.name = try  ~~~~


이런식으로 하고 싶은데 name이라는 키가 json에 없을 때 에러는 안났음 좋겠어 ㅎ

일때는 바로,


self.name = try container.decodeIfPresent(String.self, forKey: .name)


이런식으로 하면 됩니다.


뭐가 달라졌나요!!?!?!?

바로 decode 에서 decodeIfPresent로 바뀌었다는 점이죠.

decode와 decodeIfPresent의 차이점은 바로 리턴타입인데요, 

decode는 옵셔널이 아닌 타입. 



public func decode(_ type: String.Type, forKey key: KeyedDecodingContainer.Key

throws -> String



decodeIfPresent는 옵셔널타입을 리턴합니다.

 


public func decodeIfPresent(_ type: String.Type, forKey key: KeyedDecodingContainer.Key

throws -> String?


그래서  이 decodeIfPresent를 쓰려면 이걸 받는 타입이 옵셔널로 지정되어야 합니다. 



엄청 헷갈리죠...

저도 헷갈림..


내 프로퍼티가 옵셔널인가?가 제일 중요한 부분인것 같아요.

옵셔널이 아니면, 커스텀 디코딩 init에서 try?를 쓰면 전체를 ()!해줘야하는 찜찜함이 있으니, try를 쓰면 좋은데, 만약 해당 키가 없을것을 대비해 decodeIfPresent을 사용해줘야하죠. 하지만 해당 키가 있어도 null일 수 있는 건 함정ㅋ


만약 내 프로퍼티가 옵셔널이면, try?든 try든 써도 되지만, 

내 프로퍼티 타입이 옵셔널임에도 불구하고, try를 쓰면



    let jsonString = """

{

    "contents" : [

        {

            "name" : null, (또는 name이라는 key가 없이 올 때 == age만 올 때)

             "age": 10


        },

        {

            "name": "alan",

            "age": 20


        }

    ]


}

""".data(using: .utf8)!



이런상황에서 상당히 이상하게 되는데요, 

원래 위에서는 프로퍼티만 옵셔널로 선언하거나 (커스텀 디코딩 로직 init이 없을경우) 또는 try?로 커스텀 디코딩을 시도하면


nil Optional(10)

Optional("alan") Optional(20)


이런식으로 해당 부분에서만 nil이 나오고 다른건 잘 파싱이 잘 되잖아요? 여기서

try?를 try로 바꾸면 (없을 수 있는 프로퍼티에 대해 ㅇㅇ) 



required init(from decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.name = try container.decode(String.self, forKey: .name)

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

    }




이렇게 하면 



   let zedd = try! JSONDecoder().decode(Contents.self, from: self.jsonString)



여기서 valueNotFound에러가 나게 됩니다. 뭐 당연하겠죠!!

try자체가 에러를 일으킬 수 있다는걸 전제하고 있으니.. 여기를 try!말고 try?로 고치되면 에러는 안나겠지만, 




나머지 애들도 파싱에 실패하여 모두 nil이게 됩니다. 

아니 


바로 여기서 2가지 선택사항이 있게 되는 것이죠. 


decodeIfPresent를 쓰던가 try?를 쓰던가



required init(from decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.name = try? container.decode(String.self, forKey: .name)

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

    }



 required init(from decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.name = try container.decodeIfPresent(String.self, forKey: .name)

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

    }



이렇게 하면 


nil Optional(10)

Optional("alan") Optional(20)


가 잘 나오는 것을 볼 수 있습니다.

저는 try?에 한표 ㅎ



+ ) decode에서 옵셔널 타입을 넘겨주는 것과 decodeIfPresent차이점. 


바로 키가 존재하냐 안하냐에 있습니다.


    let jsonString = """

{

    "contents" : [

        {

             "age": 10

        },

        {

            "name": "alan",

            "age": 20

 

        }

    ]


}

""".data(using: .utf8)!


키가 없을 수도 있는 상황. 

에서 


 required init(from decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.name = try container.decode(String?.self, forKey: .name)

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

    }


는 아무소용이 없습니다 ㅎ

keyNotFound에러로 디코딩에 실패합니다. 이럴땐 위에서 말했듯이 decodeIfPresent를 사용하는게 좋겠죠.  try?라던가..

없는 key를 String?타입으로 파싱하려고 하니까요. 


    let jsonString = """

{

    "contents" : [

        {

            "name": null,

             "age": 10

            

        },

        {

            "name": "alan",

            "age": 20


        }

    ]


}

""".data(using: .utf8)!




하지만 이런 상황. key가 오긴오는데 value가 있을수도, 없을 수도 있는 상황에서



 required init(from decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.name = try container.decode(String?.self, forKey: .name)

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

    }



은 가능하죠. 


nil Optional(10)

Optional("alan") Optional(20)


근데 이때까지 구구절절 try?냐 try냐 decode냐 decodeIfPresent냐 String이냐 String?이냐...많은 상황을 봤는데, 

해결책은 옵셔널타입의 프로퍼티와 try?다!....라고 생각하실 수 있는데..

하지만 프로퍼티를 옵셔널로 선언하면 이 프로퍼티를 사용할때 또 귀찮아지죠.  왜냐면 한번 풀어줘야 하니까 ^_ㅠ;;;


저같은 경우에는 일단 key는 전부 옴 && 근데 value는 nil일 수 있음의 경우가 많은데...근데 사용할때 옵셔널 풀어주기 귀찮으니까 옵셔널로 선언은 안하고 싶어!!!!!

라고 할때는 ㅎㅎ..



class Zedd: Decodable {

    var name: String

    var age: Int


    private enum CodingKeys: String, CodingKey {

        case name

        case age

    }


    required init(from decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.name = (try? container.decode(String.self, forKey: .name)) ?? ""

        self.age = (try? container.decode(Int.self, forKey: .age)) ?? 0

    }

}


이게 제일 깔끔한거 같습니다...

또는..


class Zedd: Decodable {

    var name: String

    var age: Int



    private enum CodingKeys: String, CodingKey {

        case name

        case age

    }


    required init(from decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.name = (try container.decode(String?.self, forKey: .name)) ?? ""

        self.age = (try container.decode(Int?.self, forKey: .age)) ?? 0

    }

}


차이점은 try -> try? / String -> String?

ㅎㅎㅎ...


글을 쭉 봤는데 이 글은 진짜 저 아니면 이해를 못할 것 같은데..........이해 가시나요?..........................................

^_ㅠ


궁금하신 점이나 지적할 점 또는 개선하면 좋을 점..?등은 언제나 환영입니다. 댓글이나 PC화면 오른쪽 하단에서 볼 수 있는 채널 서비스를 이용하여 메세지 주세요 :) 


반응형

'iOS' 카테고리의 다른 글

iOS 12 달라진점!!  (2) 2018.09.18
iOS12 ) Notification  (2) 2018.09.13
iOS ) NavigationBar  (3) 2018.08.19
iOS 12 Beta 설치 방법  (0) 2018.07.31
iOS ) UIStatusBar  (5) 2018.07.28