[Swift] Codable - Decoding 방법

728x90

이전 포스트는 Codable의 Encodable에 대해 알아 보았습니다.

이번 포스트는 Codable의 Decodable에 대해 알아보도록 하겠습니다.

 

Decodable


JSON 데이터를 디코딩 하는 방법에 대해 알아보도록 하겠습니다.
Decodable을 이용하면 JSON을 쉽게 처리할 수 있습니다.

 

# 디코딩 방법

decode 메서드는 두개의 파라미터가 필요합니다.

첫번째 파라미터는 디코딩할 타입을 정의한다.
반드시 Decodable이나 Codable 프로토콜을 채택해야됩니다.

struct Person: Codable {
   var name: String
   var age: Int
   var birthDate: String
   var address: String?
}

두번째 파라미터에는 json이 저장되어있는 data를 전달합니다.

let jsonData =
"""
{
"name" : "Jinnify",
"age" : 29,
"birthDate" : "2019-04-29T01:30:00Z",
"address" : "동대문구 답십리"
}
""".data(using: .utf8)!

결과적으로 decode방법

let decoder = JSONDecoder()

do {
  let result = try decoder.decode(Person.self, from: jsonData)
  print(result)
} catch {
  print(error)
}

 

# JSON의 Key와 프로퍼티가 동일하지 않은 경우

위와 같이 JSON의 Key가 Codable을 따르는 프로퍼티와 이름이 동일할 경우는 간단하게 처리됩니다.
하지만 JSON의 Key와 타입의 이름을 다르게 하고자 한다면 CodingKeys를 정의 해주어야 합니다.

서버에서 JSON이 아래와 같이 내려왔다고 해보죠

let jsonData =
"""
{
"name" : "Jinnify",
"age" : 29,
"birth_date" : "1991-04-29",
"address" : "동대문구 답십리"
}
""".data(using: .utf8)!

자세히 보면 birth_date 처럼 snake_case형태로 내려왔습니다.
사실 Swift는 snake_case보다 lowerCamelCase를 사용합니다.

lowerCamelCase로 변환하고 싶거나, 커스텀으로 key이름을 변환하고 싶을때
아래와 같이 CodingKeys 를 정의합니다.

  1. birth_date -> birthDate로 변경
  2. address를 juso로 변경
struct Person: Codable {
  var name: String
  var age: Int
  var birthDate: String
  var juso: String?

  enum CodingKeys: String, CodingKey {
    case name, age
    case birthDate = "birth_date"
    case juso = "address"
  }
}

let decoder = JSONDecoder()

do {
  let result = try decoder.decode(Person.self, from: jsonData)
  print(result)
} catch {
  print(error)
}

/*
결과

Person(name: "Jinnify", age: 29, birthDate: "1991-04-29", juso: Optional("동대문구 답십리"))
*/

아래와 같이 접근 가능!

let result = try decoder.decode(Person.self, from: jsonData)

result.juso
result.birthDate
result.age
result.name

💡간단하게 snake_case로 jsonData가 내려올경우 lowerCamelCase만 변환하는 방법

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

 

# Date Decoding 방법

구조체의 birthDate를 String에서 Date타입으로 변경하고, jsonData의 birthDate는 ISO8601형식을 변경하였다.

let jsonData =
"""
{
"name" : "Jinnify",
"age" : 29,
"birthDate" : "2019-04-29T01:30:00Z",
"address" : "동대문구 답십리"
}
""".data(using: .utf8)!

struct Person: Codable {
  var name: String
  var age: Int
  var birthDate: Date
  var address: String?
}

위의 Date타입으로 지정해주었지만 에러가 발생합니다. - typeMismatch

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

별도의 Date의 디코딩을 해주지 않으면 Double값으로 디코딩을 시도합니다.
birthDate의 형식을 보면 ISO8601 표준을 사용하고 있습니다.
아래와 같이 dateDecodingStrategyISO8601로 변경해주면 문제는 해결됩니다.

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

do {
  let result = try decoder.decode(Person.self, from: jsonData)
  print(result)
} catch {
  print(error)
}

/*
결과

Person(name: "Jinnify", age: 29, birthDate: 2019-04-29 01:30:00 +0000, address: Optional("동대문구 답십리"))
*/

 

# JSON의 Key와 Value가 변경되거나 null인 경우

서버의 데이터를 믿지마라!

서버에서 내려주는 JSON데이터는 변경될 수 있습니다.
실무에선 어느 상황이든 대비를 해야되기 됩니다.

기존 데이터 :

{
    "name" : "Jinnify",
    "age" : 29
}

변경된 데이터 :
age가 더이상 필요없어져서 더 이상 서버에서 내려오지 않는 경우

{
    "name" : "Jinnify",
}

age라는 데이터가 쓸모가 없어져 내려오던 데이터가 갑자기 내려오지 않는다면, keyNotFound 에러가 발생합니다.

let jsonStr = """
{
  "name" : "Jinnify",
}
"""

struct Person: Codable {
  var name: String
  var age: Int
}

let decoder = JSONDecoder()

do {
  let result = try decoder.decode(Person.self, from: jsonData)
  print(result)
} catch {
  print(error)
}

/*
결과

keyNotFound(CodingKeys(stringValue: "age", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"age\", intValue: nil) (\"age\").", underlyingError: nil))
*/

드물긴 하지만 충분히 가능성 있는 에러이기 때문에 대비를 해야됩니다.

기존의 Codable 모델이 간단하게 사용되지만

struct Person: Codable {
  var name: String
  var age: Int
}

내부적으로는 자동으로 CodingKeys, init(from decoder: Decoder) 이 자동으로 생성되어 집니다.

struct Person: Codable {
  var name: String
  var age: Int

  private enum CodingKeys: String, CodingKey {
    case name, age
  }

  init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    name = try values.decode(String.self, forKey: .name)
    age = try values.decode(Int.self, forKey: .age)
  }
}

init(from decoder: Decoder)이 부분을 통해 직접 코드를 작성하여 기본값을 넣어주는 방식으로 에러를 해결 할 수 있습니다.

let jsonStr = """
{
  "name" : "Jinnify",
}
"""

struct Person: Codable {
  var name: String
  var age: Int

  private enum CodingKeys: String, CodingKey {
    case name, age
  }

  init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    name = (try? values.decode(String.self, forKey: .name)) ?? ""
    age = (try? values.decode(Int.self, forKey: .age)) ?? 0
  }
}

JSON의 특정 키가 없는 경우 빈 디폴트 값을 주어 해결을 했습니다.
또는 모든 타입에 옵셔널 타입 으로 처리하는 방법도 있습니다.

struct Person: Codable {
  var name: String?
  var age: Int?
}

하지만 모든 타입을 옵셔널로 처리할 경우 옵셔널 바인딩을 매번 해주어야 됨으로 번거로울 수 있습니다.

여기까지 JSONDecoder를 이용해서 Data타입을 디코딩 하는 방법과
CodingKeys를 이용해서 JSON의 키를 커스텀 방법,
dateDecodingStrategy를 통한 Date 디코딩 방법,
null일 경우 처리 default 처리 방법에 대해 알아보았습니다.