일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 코테
- MainScheduler
- cleanarchitecture
- DependencyInjection
- IOS
- GCD
- 프로그래머스
- SeSAC
- CoreBluetooth
- DispatchQueue
- combine
- MainScheduler.asyncInstance
- SwiftUI
- DiffableDataSource
- 오픈채팅방
- leetcode
- gitflow
- 등굣길
- RaceCondition
- 청년취업사관학교
- MethodSwilzzling
- rxswift
- 명품cppProgramming c++
- DynamicMemberLookup
- Realm
- data_structure
- MainScheduler.Instance
- GIT
- swift
- SRP
- Today
- Total
Do.
Swift - JSON Encoding과 Decoding - Part3 본문
만드는 어플에 JSON 데이터를 파싱해야 하는데 이게 눈으로만 보고 할려고 하니까 너무 어려워서 정리...
blog.naver.com
-
오른쪽 정렬왼쪽 정렬가운데 정렬
-
- 삭제
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
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
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. 키가 없는 데이터를 인코딩, 디코딩 할 수 있다.
'iOS' 카테고리의 다른 글
Swift - Float, Double을 Int로 바꾸기, 반올림, 올림, 내림 (0) | 2022.02.09 |
---|---|
Swift - firstIndex(of:) 가 없을 때 (0) | 2022.02.09 |
Swift - JSON Encoding과 Decoding - Part2 (0) | 2022.02.09 |
Swift - JSON Encoding과 Decoding - Part1 (0) | 2022.02.09 |
Swift - 구조체 또는 클래스의 프로퍼티 순차적으로 값 얻기 (0) | 2022.02.09 |