본문 바로가기

학습 노트/iOS (2021)

205~ 206. JSON

JSON


JSON은 Java Script Object Notation의 줄임 말로,
XML을 대체하는 개방형 데이터 표준 포맷이다.

{ "Key" : Value }

위와 같은 Dictionary와 유사한 구조로,
전체가 일반 txt로 되어있어 가독성이 높고 parsing이 편한 것이 장점이다.

기본적인 문법은 동일하며 다음과 같다.

  • '{'로 시작해서 '}'로 끝난다.
  • Key는 항상 문자열이다.
  • Value로 지정할 때 문자열은 " "로 감싸 표시하고, 수와 null은 별도의 처리 없이 직접 표시한다.
  • '[ ]'로 감싸면 배열을 의미한다.

 

Encodeing과 Decoding

Json 파일을 받으면 이를 swift가 다룰 수 있도록 encoding과 decoding을 진행해야 한다.

인스턴스의 데이터를 JSON으로 바꿀 때는 JSONEncoder를 사용하며,
encodable protocol을 사용하는 데이터라면 어떤 것이든 JSON으로 바꿀 수 있다.

JSON을 일반 인스턴스로 바꾸기 위해서는 JSONDecoder를 사용한다.
마찬가지로 decodable protocol을 사용하는 어떤 것이든 인스턴스 데이터로 바꿀 수 있다.

encodable protocol과 decodable protocol을 합쳐 codable protocol을 채용했다면
JSONEncoder와 JSONDecoder를 어려움 없이 사용할 수 있다.

 

Encoding

import UIKit

struct Person: Codable {
   var firstName: String
   var lastName: String
   var birthDate: Date
   var address: String?
}

let p = Person(firstName: "John", lastName: "Doe", birthDate: Date(timeIntervalSince1970: 1234567), address: "Seoul")

let encoder = JSONEncoder()

encoder.outputFormatting = .prettyPrinted
do {
	let jsonData = try encoder.encode(p)
	print(jsonData)
} catch {
	print(error.localizedDescription)
}
결과

99 bytes

JsonEncoder를 사용하기 위해 Person 구조체는 Codable protocol을 채용했다.
JSONEncoder는 위와 같은 패턴으로 사용하고,
이렇게 변환된 데이터는 binary 형식을 가지기 때문에 99 bytes로 출력되고,
네트워크 작업시엔 이를 그대로 사용하게 된다.

import UIKit

struct Person: Codable {
   var firstName: String
   var lastName: String
   var birthDate: Date
   var address: String?
}

let p = Person(firstName: "John", lastName: "Doe", birthDate: Date(timeIntervalSince1970: 1234567), address: "Seoul")

let encoder = JSONEncoder()
do {
	let jsonData = try encoder.encode(p)
	if let jsonStr = String(data: jsonData, encoding: .utf8) {
		print(jsonStr)
	}
} catch {
	print(error.localizedDescription)
}
결과

{"firstName":"John","lastName":"Doe","address":"Seoul","birthDate":-977072633}

제대로 변환됐는지 확인하기 위해 JSON으로 변환된 jsonStr을
utf8로 변환해 출력하도록 수정하고

import UIKit

struct Person: Codable {
   var firstName: String
   var lastName: String
   var birthDate: Date
   var address: String?
}

let p = Person(firstName: "John", lastName: "Doe", birthDate: Date(timeIntervalSince1970: 1234567), address: "Seoul")

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
do {
	let jsonData = try encoder.encode(p)
	if let jsonStr = String(data: jsonData, encoding: .utf8) {
		print(jsonStr)
	}
} catch {
	print(error.localizedDescription)
}
결과

{
  "firstName" : "John",
  "lastName" : "Doe",
  "address" : "Seoul",
  "birthDate" : -977072633
}

가독성을 높이기 위해 prettyPrinted 옵션을 사용할 수도 있다.

 

Key Encoding Strategy

import Foundation

struct Person: Codable {
   var firstName: String
   var lastName: String
   var birthDate: Date
   var address: String?
}

let p = Person(firstName: "John", lastName: "Doe", birthDate: Date(timeIntervalSince1970: 1234567), address: "Seoul")


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

do {
   let jsonData = try encoder.encode(p)
   if let jsonStr = String(data: jsonData, encoding: .utf8) {
      print(jsonStr)
   }
} catch {
   print(error)
}

encoder.keyEncodingStrategy = .convertToSnakeCase

do {
   let jsonData = try encoder.encode(p)
   if let jsonStr = String(data: jsonData, encoding: .utf8) {
      print(jsonStr)
   }
} catch {
   print(error)
}
결과

{
  "firstName" : "John",
  "lastName" : "Doe",
  "address" : "Seoul",
  "birthDate" : -977072633
}
{
  "last_name" : "Doe",
  "birth_date" : -977072633,
  "first_name" : "John",
  "address" : "Seoul"
}

JSONEncoder는 속성의 이름을 Key로 바꿀 때 lowerCamelCase를 사용한다.
이는 Swift의 문법으로 경우에 따라 외부적으로 문제가 발생하는 경우도 있다.
따라서 keyEncodingStrategy 속성을 covertToSnakeCase로 변경해
'_'를 사용하는 SnakeCase 형식을 사용하도록 설정할 수 있다.

 

Date Encoding Strategy

import Foundation

struct Person: Codable {
   var firstName: String
   var lastName: String
   var birthDate: Date
   var address: String?
}

let p = Person(firstName: "John", lastName: "Doe", birthDate: Date(timeIntervalSince1970: 0), address: "Seoul")

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

do {
   let jsonData = try encoder.encode(p)
   if let jsonStr = String(data: jsonData, encoding: .utf8) {
      print(jsonStr)
   }
} catch {
   print(error)
}

encoder.dateEncodingStrategy = .iso8601
let formatter = DateFormatter()

do {
   let jsonData = try encoder.encode(p)
   if let jsonStr = String(data: jsonData, encoding: .utf8) {
      print(jsonStr)
   }
} catch {
   print(error)
}

formatter.dateFormat = "yyyy/MM/dd"
encoder.dateEncodingStrategy = .formatted(formatter)

do {
   let jsonData = try encoder.encode(p)
   if let jsonStr = String(data: jsonData, encoding: .utf8) {
	  print(jsonStr)
   }
} catch {
   print(error)
}
결과

{
  "firstName" : "John",
  "lastName" : "Doe",
  "address" : "Seoul",
  "birthDate" : -978307200
}
{
  "firstName" : "John",
  "lastName" : "Doe",
  "address" : "Seoul",
  "birthDate" : "1970-01-01T00:00:00Z"
}
{
  "firstName" : "John",
  "lastName" : "Doe",
  "address" : "Seoul",
  "birthDate" : "1970\/01\/01"
}

JSONEncoder를 사용해 Date 인스턴스를 변환하면
현재 시간을 기준으로 표시한 형식의 데이터로 변환된다.
해당 형식은 데이터 자체의 가독성도 떨어지고 호환성도 그리 좋지 않다.

Date의 현행 표준은 ISO8601 형식이고,
해당 형식은 가독성과 호환성이 뛰어난 것이 장점이다.
간단하게 dateEncodingStrategy 속성을 iso8601로 설정하는 것으로 사용할 수 있다.

dateFormat 속성을 직접 선언하는 것으로,
정해져 있는 형식이 아닌 개발자나 사용자의 편의에 의한 형식을 사용하는 것도 가능하다.

 

JSON Decoding

import Foundation

struct Person: Codable {
   var firstName: String
   var lastName: String
   var age: Int
   var address: String?
}

let jsonStr = """
{
"firstName" : "John",
"age" : 30,
"lastName" : "Doe",
"address" : "Seoul"
}
"""

guard let jsonData = jsonStr.data(using: .utf8) else {
   fatalError()
}

let decoder = JSONDecoder()

do {
	let p = try decoder.decode(Person.self, from: jsonData)
	print(p.firstName)
	print(p.lastName)
	print(p.age)
	print(p.address)
} catch {
	print(error.localizedDescription)
}
결과

John
Doe
30
Optional("Seoul")

Decoding은 위와 같은 패턴으로 구현한다.
Decoding의 성공 여부는 JSON의 key와 속성의 이름이 동일하고,
JSON의 value의 형식과 속성의 형식이 같아야 한다.
예를 들어 Person 구조체의 속성에 lastName이 없거나, 속성의 이름이 lastname으로 다르거나,
age 속성의 형식이 문자열인 경우엔 Decoding에 실패하게 된다.

 

Key Decoding Strategy

import Foundation

struct Person: Codable {
   var firstName: String
   var lastName: String
   var age: Int
   var address: String?
}

let jsonStr = """
{
"first_name" : "John",
"age" : 30,
"last_name" : "Doe",
"address" : "Seoul"
}
"""

guard let jsonData = jsonStr.data(using: .utf8) else {
   fatalError()
}

let decoder = JSONDecoder()

decoder.keyDecodingStrategy = .convertFromSnakeCase

do {
   let p = try decoder.decode(Person.self, from: jsonData)
   print(p.firstName)
   print(p.lastName)
   print(p.age)
   print(p.address)
} catch {
   print(error)
}
결과

John
Doe
30
Optional("Seoul")

JSON 데이터가 SnakeCase를 사용하고 있기 때문에,
Person 구조체의 속성과 이름이 일치하지 않는다.
따라서 지금 상태로는 Decoding에 실패하게 된다.

만약 속성이 Optional 형식이라면 실패한 속성을 nil로 대체할 수 있지만,
firstName과 lastName은 Optional 형식이 아니라 

속성의 이름을 SnakeCase로 변경하는 것도 방법이지만,
keyDecodingStrategy 속성을 convertFromSnakeCase로 지정해,
SnakeCase로 구성된 Key를 lowerCamelCase로 변경해 decoding 진행할 수도 있다.

 

Date Decoding Strategy

import Foundation

struct Product: Codable {
   var name: String
   var releaseDate: Date
}

let jsonStr = """
{
"name" : "iPad Pro",
"releaseDate" : "2018-10-30T23:00:00Z"
}
"""

guard let jsonData = jsonStr.data(using: .utf8) else {
   fatalError()
}

let decoder = JSONDecoder()

decoder.dateDecodingStrategy = .iso8601

do {
   let p = try decoder.decode(Product.self, from: jsonData)
   dump(p)
} catch {
   print(error)
}
결과

▿ __lldb_expr_9.Product
  - name: "iPad Pro"
  ▿ releaseDate: 2018-10-30 23:00:00 +0000
    - timeIntervalSinceReferenceDate: 562633200.0

JSON의 Date 데이터를 Decoding 할 때는 dateDecodingStrategy 속성을 iso8601로 설정해 사용한다.
이렇게 decoding을 진행할 경우 timeIntervalSinceReferenceDate 값도 함께 반환되는데,
iso8601로 설정하지 않는 기본 상태에서도 반환되는 값이므로, 해당 값이 필요하면 기본 값으로 그대로 사용해도 무관하다.

단, JSON의 releaseDate의 형식이 ISO8601이고, 이를 Double로는 변환할 수 없기 때문에 오류가 발생한다.

 

Custom Key Mapping

import Foundation

struct Person: Codable {
   var firstName: String
   var lastName: String
   var age: Int
   var address: String?
}

let jsonStr = """
{
"firstName" : "John",
"age" : 30,
"lastName" : "Doe",
"homeAddress" : "Seoul",
}
"""

guard let jsonData = jsonStr.data(using: .utf8) else {
   fatalError()
}

let decoder = JSONDecoder()

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

▿ __lldb_expr_14.Person
  - firstName: "John"
  - lastName: "Doe"
  - age: 30
  - address: nil

Decoding에는 성공하지만 JSON의 homeAddress와 Person의 address는 key 이름이 일치하지 않아
부분적으로 실패한 결과를 반환한다.

이러한 경우 Person 구조체의 속성 이름을 변경해 해결하는 것도 방법이지만,
JSONDecoder는 Key를 일치시킬 수 있도록 KeyMapping을 지원하니 이를 사용하는 것도 좋다.

import Foundation

struct Person: Codable {
   var firstName: String
   var lastName: String
   var age: Int
   var address: String?
   
   enum CodingKeys: String, CodingKey {
        case firstName
        case lastName
        case age
        case address = "homeAddress"
    }
}

let jsonStr = """
{
"firstName" : "John",
"age" : 30,
"lastName" : "Doe",
"homeAddress" : "Seoul",
}
"""

guard let jsonData = jsonStr.data(using: .utf8) else {
   fatalError()
}

let decoder = JSONDecoder()

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

▿ __lldb_expr_16.Person
  - firstName: "John"
  - lastName: "Doe"
  - age: 30
  ▿ address: Optional("Seoul")
    - some: "Seoul"

KeyMapping은 위와 같이 형식의 안에서 열거형으로 작성한다.
따로 KeyMapping을 선언하지 않으면 자동으로 기본 Mapping이 생성되며,
하나라도 수동으로 정의하기 시작하면 자동으로 생성되지는 않는다.
case의 이름은 속성의 이름과 동일해야 하고,
rawValue로 지정하는 값에 따라 매핑이 이뤄지게 된다.

따라서 위에서 선언한 KeyMapping 대로라면
JSON의 homeAddress 데이터는 address 속성에 자동으로 변환돼 저장된다.

 

Custom Encoding

import Foundation

enum EncodingError: Error {
   case unknown
   case invalidRange
}

struct Employee: Codable {
   var name: String
   var age: Int
   var address: String?

	func encode(to encoder: Encoder) throws {
		var container = encoder.container(keyedBy: CodingKeys.self)
		try container.encode(name, forKey: .name)
		guard (30...60).contains(age) else {
			throw EncodingError.invalidRange
		}
		try container.encode(age, forKey: .age)
		try container.encodeIfPresent(address, forKey: .address)
	}
}

let p = Employee(name: "James", age: 35, address: "Seoul")

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

do {
   let jsonData = try encoder.encode(p)
   if let jsonStr = String(data: jsonData, encoding: .utf8) {
      print(jsonStr)
   }
} catch {
   print(error)
}

let p2 = Employee(name: "James", age: 20, address: "Seoul")

do {
   let jsonData = try encoder.encode(p2)
   if let jsonStr = String(data: jsonData, encoding: .utf8) {
	  print(jsonStr)
   }
} catch {
   print(error)
}
결과

{
  "name" : "James",
  "age" : 35,
  "address" : "Seoul"
}
invalidRange

Custom Encoding을 진행하는 경우는 두 가지다.

  • 값을 검증하거나 제약을 추가해야 하는 경우
  • Encoding 전략이나 Custom으로 원하는 결과를 얻을 수 없는 경우

encode 함수의 파라미터로 전달된 encoder에 접근하기 위해서는 container를 사용해야 한다.
그중 각각의 속성들에 접근할 수 있는 Key들은 CodingKeys에 저장돼있다.
각각의 속성들을 encoding 할 때는 encode(forKey:)를 사용하고,
guard문 등을 사용해 조건부로 encoding을 진행하는 방식이다.
age의 값이 30~60의 범위 내에 존재하는 경우 encoding을 진행하도록 구현했으므로
범위를 벗어나는 p2는 invalidRange를 출력한다.

 

CustomDecoding

import Foundation

enum DecodingError: Error {
   case unknown
   case invalidRange
}

struct Employee: Codable {
   var name: String
   var age: Int
   var address: String?
   
	init(from decoder: Decoder) throws {
		let container = try decoder.container(keyedBy: CodingKeys.self)
		
		name = try container.decode(String.self, forKey: .name)
		age = try container.decode(Int.self, forKey: .age)
		guard (30...60).contains(age) else {
			throw DecodingError.invalidRange
		}
		
		address = try container.decodeIfPresent(String.self, forKey: .address)
	}
}

let jsonStr = """
{
"name" : "John",
"age" : 20,
"address" : "Seoul"
}
"""

guard let jsonData = jsonStr.data(using: .utf8) else {
   fatalError()
}

let decoder = JSONDecoder()

do {
   let e = try decoder.decode(Employee.self, from: jsonData)
   dump(e)
} catch {
   print(error)
}

CustomEncoding과 비슷한 방식으로 구현한다.
차이점은 다음과 같다.

  • decode(forKey:) 메서드를 사용한다.
  • decode(forKey:) 메서드의 첫 번째 파라미터는 '형식'을 전달한다.
  • 조건을 판단하고 Decoding을 진행하는 것이 아니라 Decoding을 진행하고 조건을 판단한다.

'학습 노트 > iOS (2021)' 카테고리의 다른 글

208. Data Task  (0) 2022.08.22
207. URL Loading System  (0) 2022.08.20
204. App Transport Security (ATS)  (0) 2022.07.08
203. Display Web Contents  (0) 2022.07.06
202. Networking  (0) 2022.07.06