Do.

Swift - JSON Encoding과 Decoding - Part3 본문

iOS

Swift - JSON Encoding과 Decoding - Part3

Hey_Hen 2022. 2. 9. 15:58

 

이미지 썸네일 삭제
Swift - JSON Encoding과 Decoding - Part1

만드는 어플에 JSON 데이터를 파싱해야 하는데 이게 눈으로만 보고 할려고 하니까 너무 어려워서 정리...

blog.naver.com

  • 오른쪽 정렬왼쪽 정렬가운데 정렬
  •  
  • 삭제
이미지 썸네일 삭제
Swift - JSON Encoding과 Decoding - Part2

Part1 주소: https://blog.naver.com/raphaelra44/222460995852 길어서 파트를 나눈다기 보다는 이제부터 ...

blog.naver.com

JSON 데이터를 인코딩 하고 디코딩 하는 필수적인 내용은 앞선 파트에서 모두 다룬 것 같다.

Part3 좀더 유용하게 쓸 수 있는 내용이 될 것 같다.

 

레퍼런스: https://www.raywenderlich.com/3418439-encoding-and-decoding-in-swift#toc-anchor-007

이미지 썸네일 삭제
Encoding and Decoding in Swift

In this tutorial, you’ll learn all about encoding and decoding in Swift, exploring the basics and advanced topics like custom dates and custom encoding.

www.raywenderlich.com

 

Date Encoding, Decoding

Date를 따로 인코딩 하고 디코딩 하는 법이 필요한가 하면 그렇다이다.

import Foundation

struct Person: Codable {
  var name: String
  var age: String
  var gender: String
  var birthday: Date
  var majorJob: Job
}

struct Job: Codable {
  var jobTitle: String
}

let henrysJob = Job(jobTitle: "Developer")
let date = DateFormatter()
date.locale = Locale(identifier: "ko_kr")
date.dateFormat = "yyyy-MM-dd"
let henrysBirthDay = date.date(from: "1993-05-15") ?? Date()
let henry = Person(name: "henry", age: "28", gender: "male", birthday: henrysBirthDay, majorJob: henrysJob)

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted]
let encodeData = try encoder.encode(henry)
 

위와 같은 코드가 있을 때 인코딩 된 데이터를 보면

{
  "age" : "28",
  "gender" : "male",
  "majorJob" : {
    "jobTitle" : "Developer"
  },
  "name" : "henry",
  "birthday" : -240915600
}
 

이런식으로 birthday가 뭔 값인지 모를 숫자가 나온다.

henry.birthday //"May 15, 1993 at 12:00 AM"
 

저장된 birthday가 제대로 들어갔나 확인해보아도 정상적으로 들어갔다.

그러니까 인코딩 되는 과정에서 무언가 잘못 되었다는 뜻이다.

그 이유는 JSON의 날짜 표기에 대한 특별한 표준이 없기 때문에 JSONEncoder와 Decoder가 임의로 값을 표시하기 때문이다.

 

그래서 Encoder, Decoder의 dateEncodingStrategy를 사용해서 어떤 포멧의 날짜가 들어가 있는지 확인 시켜줘야 한다.(우선 인코더 부터 다룸)

 

우선 인코딩 하기 전 Date의 포맷이 "yyyy-MM-dd"형태이므로 이를 dateEncodingStrategy에 전달 해주어야 한다. dateEncodingStrategy에는 열거형으로 어떤 형태가 있는지는 레퍼런스 참조

https://developer.apple.com/documentation/foundation/jsonencoder/dateencodingstrategy

Apple Developer Documentation

An unknown error occurred.  Developer Documentation Discover iOS iPadOS macOS tvOS watchOS Safari and Web Games Business Education WWDC Design Human Interface Guidelines Resources Videos Apple Design Awards Fonts Accessibility Localization Accessories Develop Xcode Swift Swift Playgrounds TestFligh...

developer.apple.com

중에서 Custom Formatt의 .formatted를 사용할 것이다.

import Foundation

struct Person: Codable {
  var name: String
  var age: String
  var gender: String
  var birthday: Date
  var majorJob: Job
}

struct Job: Codable {
  var jobTitle: String
}

let henrysJob = Job(jobTitle: "Developer")
let date = DateFormatter()
date.locale = Locale(identifier: "ko_kr")
date.dateFormat = "yyyy-MM-dd"
let henrysBirthDay = date.date(from: "1993-05-15") ?? Date()
let henry = Person(name: "henry", age: "28", gender: "male", birthday: henrysBirthDay, majorJob: henrysJob)

henry.birthday

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted]
encoder.dateEncodingStrategy = .formatted(.dateFormatter)
let encodeData = try encoder.encode(henry)

print(String(data: encodeData, encoding: .utf8)!)

extension DateFormatter {
  static let dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd"
    return formatter
  }()
}
 

확인해 보아야 할 부분은

1. extension DateFormatter 로 사용하고자 하는 데이터 포맷을 dateFormatter로 생성하고

encoder 구성에서 dateEncodingStrategy로 .formatted(.dateFormatter)를 전달했다. 출력 결과

{
  "name" : "henry",
  "age" : "28",
  "gender" : "male",
  "birthday" : "1993-05-15",
  "majorJob" : {
    "jobTitle" : "Developer"
  }
}
 

날짜가 원하는대로 잘 표시 된다.

 

이와 마찬가지인 이유와 원리로 디코딩 시에도 어떤 포맷인지 전달해야지 올바른 데이터를 얻을 수 있다.

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(.dateFormatter)
let decodeData = try decoder.decode(Person.self, from: encodeData)
print(decodeData.birthday)
//1993-05-14 15:00:00 +0000 (시간존 설정 안해서 이런 듯)
 

한번 dateDecodingStrategy 부분을 코멘트 처리 해버리면 디코딩 자체를 실패 해 버린다.

Playground execution terminated: An error was thrown and was not caught:
▿ DecodingError
  ▿ typeMismatch : 2 elements
    - .0 : Swift.Double
    ▿ .1 : Context
      ▿ codingPath : 1 element
        - 0 : CodingKeys(stringValue: "birthday", intValue: nil)
      - debugDescription : "Expected to decode Double but found a string/data instead."
      - underlyingError : nil
 

 

서브클래스 인코딩 및 디코딩

상속관계에 있는 서브클래스의 경우 인코딩은 또 다른 방법을 취해야 한다.

 

예를들어 지금 Person과 Job객체는 구조체로 이루어 져있는데 BasePerson 그를 상속받은 JobPerson 클래스를 따로 구분했다고 하자.

import Foundation

class BasePerson: Encodable {
  var name: String
  var age: String
  var gender: String
  var birthday: Date
  
  init(name: String, age: String, gender: String, birthday: Date) {
    self.name = name
    self.age = age
    self.gender = gender
    self.birthday = birthday
  }
}

class JobPerson: BasePerson {
  var majorJob: Job
  
  init(name: String, age: String, gender: String, birthday: Date, majorJob: Job) {
    self.majorJob = majorJob
    super.init(name: name, age: age, gender: gender, birthday: birthday)
  }
}

struct Job: Encodable {
  var jobTitle: String
}
 
//date setup
let date = DateFormatter()
date.locale = Locale(identifier: "ko_kr")
date.dateFormat = "yyyy-MM-dd"

//henry
let henrysBirthDay = date.date(from: "1993-05-15") ?? Date()
let henry = BasePerson(name: "henry", age: "28", gender: "male", birthday: henrysBirthDay)

//epi
let episJob = Job(jobTitle: "Developer")
let epiBirthday = date.date(from: "1993-02-02") ?? Date()
let epi = JobPerson(name: "epi", age: "28", gender: "female", birthday: epiBirthday, majorJob: episJob)


let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted]
encoder.dateEncodingStrategy = .formatted(.dateFormatter)

let henryEncodeData = try encoder.encode(henry)
let epiEncodeData = try encoder.encode(epi)
print(String(data: henryEncodeData, encoding: .utf8)!)
print(String(data: epiEncodeData, encoding: .utf8)!)
 
{
  "age" : "28",
  "gender" : "male",
  "name" : "henry",
  "birthday" : "1993-05-15"
}
{
  "age" : "28",
  "gender" : "female",
  "name" : "epi",
  "birthday" : "1993-02-02"
}
 

JobPerson는Encodable이 아니라 api의 Job 속성이 누락되어서 나왔다.

 

인코딩 되는 코드 부터 설명하자면

import Foundation

class BasePerson: Encodable {
  var name: String
  var age: String
  var gender: String
  var birthday: Date
  
  init(name: String, age: String, gender: String, birthday: Date) {
    self.name = name
    self.age = age
    self.gender = gender
    self.birthday = birthday
  }
}

class JobPerson: BasePerson {
  var majorJob: Job
  
  init(name: String, age: String, gender: String, birthday: Date, majorJob: Job) {
    self.majorJob = majorJob
    super.init(name: name, age: age, gender: gender, birthday: birthday)
  }
  
  enum CodingKeys: CodingKey {
    case person, job
  }
  
  override func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(majorJob, forKey: .job)
    let baseEncoder = container.superEncoder(forKey: .person)
    try super.encode(to: baseEncoder)
  }
}

struct Job: Encodable {
  var jobTitle: String
}
 

변경된점은 JobPerson 서브 클래스가 CodingKeys 열거형과 encode 오버라이드 함수를 가지게 되었다.

함수 내용은 크게 별것 없으므로 찬찬히 읽어보자

 

{
  "age" : "28",
  "gender" : "male",
  "name" : "henry",
  "birthday" : "1993-05-15"
}
{
  "job" : {
    "jobTitle" : "Developer"
  },
  "person" : {
    "age" : "28",
    "gender" : "female",
    "name" : "epi",
    "birthday" : "1993-02-02"
  }
}
 

epi의 경우 JSON 데이터를 보면 job과 person으로 구분되어 출력되었다.

 

디코딩의 경우

class BasePerson: Codable {
  var name: String
  var age: String
  var gender: String
  var birthday: Date
  
  init(name: String, age: String, gender: String, birthday: Date) {
    self.name = name
    self.age = age
    self.gender = gender
    self.birthday = birthday
  }
}

class JobPerson: BasePerson {
  var majorJob: Job
  
  init(name: String, age: String, gender: String, birthday: Date, majorJob: Job) {
    self.majorJob = majorJob
    super.init(name: name, age: age, gender: gender, birthday: birthday)
  }
  
  enum CodingKeys: CodingKey {
    case person, job
  }
  
  required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    majorJob = try container.decode(Job.self, forKey: .job)
    let baseDecoder = try container.superDecoder(forKey: .person)
    try super.init(from: baseDecoder)
  }
  
  
  override func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(majorJob, forKey: .job)
    let baseEncoder = container.superEncoder(forKey: .person)
    try super.encode(to: baseEncoder)
  }
}

struct Job: Codable {
  var jobTitle: String
}
//date setup
let date = DateFormatter()
date.locale = Locale(identifier: "ko_kr")
date.dateFormat = "yyyy-MM-dd"

//henry
let henrysBirthDay = date.date(from: "1993-05-15") ?? Date()
let henry = BasePerson(name: "henry", age: "28", gender: "male", birthday: henrysBirthDay)

//epi
let episJob = Job(jobTitle: "Developer")
let epiBirthday = date.date(from: "1993-02-02") ?? Date()
let epi = JobPerson(name: "epi", age: "28", gender: "female", birthday: epiBirthday, majorJob: episJob)


let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted]
encoder.dateEncodingStrategy = .formatted(.dateFormatter)

let henryEncodeData = try encoder.encode(henry)
let epiEncodeData = try encoder.encode(epi)
print(String(data: henryEncodeData, encoding: .utf8)!)
print(String(data: epiEncodeData, encoding: .utf8)!)


let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(.dateFormatter)
let henryDecodeData = try decoder.decode(BasePerson.self, from: henryEncodeData)
let epiDecodeData = try decoder.decode(JobPerson.self, from: epiEncodeData)
 

우선 BasePerson과 Job이 Encodable에서 Codable(Decodable을 포함함)로 변경되었고

그에 따라서 JobPerson에 required init가 추가되었다. 내용은 크게 별거 없다.

 

 

여러 타입이 혼합된 배열 유형 JSON 데이터

위에서 작성한 데이터에서 연결된 내용인데

{
  "age" : "28",
  "gender" : "male",
  "name" : "henry",
  "birthday" : "1993-05-15"
}
{
  "job" : {
    "jobTitle" : "Developer"
  },
  "person" : {
    "age" : "28",
    "gender" : "female",
    "name" : "epi",
    "birthday" : "1993-02-02"
  }
}
 

이 내용이 지금은 henryEncodeData, epiEncodeData 따로따로 출력한 내용인데 요청시 함께 올 수 있다. 배열의 형태로

[
  {
    "age" : "28",
    "gender" : "male",
    "name" : "henry",
    "birthday" : "1993-05-15"
  }
  {
    "job" : {
      "jobTitle" : "Developer"
    },
    "person" : {
      "age" : "28",
      "gender" : "female",
      "name" : "epi",
      "birthday" : "1993-02-02"
    }
  }
]
 

[BasePerson, JobPerson]의 형태로 JSON 데이터가 생성, 전달되었다.

 

이와같은 경우는 열거형으로 해결할 수 있다.

import Foundation

struct Job: Encodable {
  var name: String
}

enum AnyPerson: Encodable {
  case defaultPerson(name: String, age: String, gender: String, birthday: Date)
  case customPerson(name: String, age: String, gender: String, birthday: Date, majorJob: Job)
  case noPerson
  
  enum CodingKeys: CodingKey {
    case name, age, gender, birthday, job
  }
  
  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    
    switch self {
    case .defaultPerson(name: let name, age: let age, gender: let gender, birthday: let birthday):
      try container.encode(name, forKey: .name)
      try container.encode(age, forKey: .age)
      try container.encode(gender, forKey: .gender)
      try container.encode(birthday, forKey: .birthday)
    case .customPerson(name: let name, age: let age, gender: let gender, birthday: let birthday, majorJob: let majorJob):
      try container.encode(name, forKey: .name)
      try container.encode(age, forKey: .age)
      try container.encode(gender, forKey: .gender)
      try container.encode(birthday, forKey: .birthday)
      try container.encode(majorJob, forKey: .job)
    case .noPerson:
      let context = EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "Invalid 'Person'")
      throw EncodingError.invalidValue(self, context)
    }
  }
}

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted]
encoder.dateEncodingStrategy = .formatted(.dateFormatter)
let persons = [AnyPerson.defaultPerson(name: "henry", age: "28", gender: "male", birthday: Date()),
               AnyPerson.customPerson(name: "epi", age: "28", gender: "female", birthday: Date(), majorJob: Job(name: "Developer"))]
let encodePersonsData = try encoder.encode(persons)
print(String(data: encodePersonsData, encoding: .utf8)!)
 

서브클래스 인코딩, 디코딩 했던 데이터를 옮겼는데 이번에는 Job 구조체는 그대로 남아있고 Job을 가진 Person과 그렇지 않은 Person이 클래스가 아닌 열거체로 AnyPerson 이름으로 합쳐서 구성되었다.

 

속성 구성에 따른 Person의 종류를 Case로 구분하고 encode 메소드에서 스위치에 따라 다르게 인코딩 한다.

결과적으로 당초 원했던 JSON 데이터 형태가 나온다.

[
  {
    "age" : "28",
    "gender" : "male",
    "name" : "henry",
    "birthday" : "2021-08-08"
  },
  {
    "age" : "28",
    "gender" : "female",
    "name" : "epi",
    "birthday" : "2021-08-08",
    "job" : {
      "name" : "Developer"
    }
  }
]
 

 

디코딩의 경우 AnyPerson의 Extension 하여 추가한다.

extension AnyPerson: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    let containerKeys = Set(container.allKeys)
    let defaultKeys = Set<CodingKeys>([.name, .age, .gender, .birthday])
    let customKeys = Set<CodingKeys>([.name, .age, .gender, .birthday, .job])
    
    switch containerKeys {
    case defaultKeys:
      let name = try container.decode(String.self, forKey: .name)
      let age = try container.decode(String.self, forKey: .age)
      let gender = try container.decode(String.self, forKey: .gender)
      let birthday = try container.decode(Date.self, forKey: .birthday)
      self = .defaultPerson(name: name, age: age, gender: gender, birthday: birthday)
    case customKeys:
      let name = try container.decode(String.self, forKey: .name)
      let age = try container.decode(String.self, forKey: .age)
      let gender = try container.decode(String.self, forKey: .gender)
      let birthday = try container.decode(Date.self, forKey: .birthday)
      let job = try container.decode(Job.self, forKey: .job)
      self = .customPerson(name: name, age: age, gender: gender, birthday: birthday, majorJob: job)
    default:
      self = .noPerson
    }
  }
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(.dateFormatter)
let decodePersonsData = try decoder.decode([AnyPerson].self, from: encodePersonsData)
 

개념이 인코딩이랑 비슷해서 크게 어렵지는 않은 편

 

키가 없는 컨테이너

키가 없는 컨테이너는 말 그대로 키 없이 내보낼 수 있는 JSON 데이터로

[
  "Developer",
  "DataScience",
  "NetworkEngineer"
]
 

위와 같이 데이터를 작성하고 싶을 때 방법 중 하나다

import Foundation

struct Job: Codable {
  var name: String
}

struct Label: Encodable {
  var jobTitles: [Job]
  
  func encode(to encoder: Encoder) throws {
    var container = encoder.unkeyedContainer()
    for jobTitle in jobTitles {
      try container.encode(jobTitle.name)
    }
  }
}

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted

let job = Label(jobTitles: [Job(name: "Developer"), Job(name: "DataScience"), Job(name: "NetworkEngineer")])
let encodeData = try encoder.encode(job)

print(String(data: encodeData, encoding: .utf8)!)
 

Job 구조체를 배열로 저장한 데이터이고 원래 방법으로 했다면 데이터는

[
  "name" : "Developer",
  "name" : "DataScience",
  "name" : "NetworkEngineer"
]
 

요런식으로 나왔을 것이다,

그래서 job을 Label 구조체로 감싸고, container를 언키 컨테이너로 선언 하여 위와 같이 코딩하면

[
  "Developer",
  "DataScience",
  "NetworkEngineer"
]
 

이런 결과물이 나온다.

 

디코드는 어떻게 될까?

Label을 extension으로 Decodable을 작성하는데

extension Label: Decodable {
  init(from decoder: Decoder) throws {
    var container = try decoder.unkeyedContainer()
    var jobs: [Job] = []
    while !container.isAtEnd {
      let job = try container.decode(String.self)
      jobs.append(Job(name: job))
    }
    jobTitles = jobs
  }
}

let decoder = JSONDecoder()

let decodeData = try decoder.decode(Label.self, from: encodeData)

decodeData.jobTitles.forEach { job in
  print(job)
}
 
Job(name: "Developer")
Job(name: "DataScience")
Job(name: "NetworkEngineer")
 

container.isAtEnd를 통해 반복해서 배열 데이터를 decode한다.

마지막에 jobTitles = jobs는 꼭 있어야 하는 부분이니 주의

 

 

Part3 정리

1 .Date를 원활하게 인코딩, 디코딩 할 수 있다.

2. 서브클래스 인코딩, 디코딩 할 수 있다.

3. 여러 타입이 혼합된 데이터를 인코딩, 디코딩 할 수 있다.

4. 키가 없는 데이터를 인코딩, 디코딩 할 수 있다.

 

Comments