{
"name": "Endeavor",
"abv": 8.9,
"brewery": "Saint Arnold",
"style": "ipa"
}
enum BeerStyle : String {
case ipa
case stout
case kolsch
// ...
}
struct Beer {
let name: String
let brewery: String
let style: BeerStyle
}
enum BeerStyle : String, Codable {
// ...
}
struct Beer : Codable {
// ...
}
let jsonData = jsonString.data(encoding: .utf8)!
let decoder = JSONDecoder()
let beer = try! decoder.decode(Beer.self, for: jsonData)
struct Beer : Codable {
// ...
enum CodingKeys : String, CodingKey {
case name
case abv = "alcohol_by_volume"
case brewery = "brewery_name"
case style
}
}
let encoder = JSONEncoder()
let data = try! encoder.encode(beer)
print(String(data: data, encoding: .utf8)!)
{"style":"ipa","name":"Endeavor","alcohol_by_volume":8.8999996185302734,"brewery_name":"Saint Arnold"}
encoder.outputFormatting = .prettyPrinted
{
"style" : "ipa",
"name" : "Endeavor",
"alcohol_by_volume" : 8.8999996185302734,
"brewery_name" : "Saint Arnold"
}
프로 팁: ISO 8601을 포함한 다양한 포멧으로 변환해보려면 nsdateformatter.com에서 해볼 수 있다.
struct Foo : Encodable {
let date: Date
}
let foo = Foo(date: Date())
try! encoder.encode(foo)
{
"date" : 519751611.12542897
}
encoder.dateEncodingStrategy = .iso8601
{
"date" : "2017-06-21T15:29:32Z"
}
- .formatted(DateFormatter): 지원하고자하는 표준 날짜 포멧 문자열이 없을때. 여러분의 date formatter 인스턴스를 제공한다.
- .custom( (Date, encoder) throws -> Void ): 아주 커스텀하고 싶을때, 여기 블럭을 전달하여 제공된 인코더로 데이터를 인코딩 할 것이다.
- .millisecondsSince1970와 .secondsSince1970: API에서 아주 일반적인 양식은 아니다. 이것은 인코딩된 표현에서 타임 지역 정보를 완전히 손실해버리기 때문에 별로 추천하지 않는다. 그런 이유로 누군가가 잘못된 가정을 생각하기 쉽게 만든다.
{
"a": "NaN",
"b": "+Infinity",
"c": "-Infinity"
}
struct Numbers : Decodable {
let a: Float
let b: Float
let c: Float
}
decoder.nonConformingFloatDecodingStrategy = .convertFromString(
positiveInfinity: "+Infinity",
negativeInfinity: "-Infinity",
nan: "NaN"
)
let numbers = try! decoder.decode(Numbers.self, from: jsonData)dump(numbers)
▿ __lldb_expr_71.Numbers
-a: inf
-b: -inf
-c: nan
- .base64
- .custom( (Data, Encoder) throws -> Void)
- .base64
- .custom( (Decoder) throws -> Data)
{
"beers": [ {...} ]
}
struct BeerList : Codable {
let beers: [Beer]
}
let decoder = JSONDecoder()
let beers = try decoder.decode([Beer].self, from: data)
[
{
"beer" : {
"id": "uuid12459078214",
"name": "Endeavor",
"abv": 8.9,
"brewery": "Saint Arnold",
"style": "ipa"
}
}
]
[[String: Beer]]
Array<Dictionary<String, Beer>>
let decoder = JSONDecoder()
let beers = try decoder.decode([[String:Beer]].self, from: data)
dump(beers)
▿ 1 element
▿ 1 key/value pair
▿ (2 elements)
- key: "beer"
▿ value: __lldb_expr_37.Beer
- name: "Endeavor"
- brewery: "Saint Arnold"
- abv: 8.89999962
- style: __lldb_expr_37.BeerStyle.ipa
{
"meta": {
"page": 1,
"total_pages": 4,
"per_page": 10,
"total_records": 38
},
"breweries": [
{
"id": 1234,
"name": "Saint Arnold"
},
{
"id": 52892,
"name": "Buffalo Bayou"
}
]
}
struct PagedBreweries : Codable {
struct Meta : Codable {
let page: Int
let totalPages: Int
let perPage: Int
let totalRecords: Int
enum CodingKeys : String, CodingKey {
case page
case totalPages = "total_pages"
case perPage = "per_page"
case totalRecords = "total_records"
}
}
struct Brewery : Codable {
let id: Int
let name: String
}
let meta: Meta
let breweries: [Brewery]
}
extension Beer {
func encode(to encoder: Encoder) throws {
}
}
struct Beer : Coding {
// ...
let createdAt: Date
let bottleSizes: [Float]
let comments: String?
enum CodingKeys: String, CodingKey {
// ...
case createdAt = "created_at",
case bottleSizes = "bottle_sizes"
case comments
}
}
- Keyed Container: 키로 값을 제공함. 이것은 원래 딕셔너리임.
- Unkeyed Container: 키 없이 정렬된 값을 제공함. JSONEncoder에서는 배열을 의미함.
- Single Value Container: 담겨진 엘리먼트의 어떤 종류도 없이 가공되지 않은 값을 만듦.
var container = encoder.container(keyedBy: CodingKeys.self)
- 컨테이너는 우리가 변경할 수 있도록 반드시 mutable 프로퍼티여야하는데, 변수를 var로 선언해야한다.
- 키들을 지정해야하는데(그리하여 프로퍼티와 키를 매핑시킨다) 이 컨테이너로 인코딩시킬 수 있는 키가 무엇인지 안다.
try container.encode(name, forKey: .name)
try container.encode(abv, forKey: .abv)
try container.encode(brewery, forKey: .brewery)
try container.encode(style, forKey: .style)
try container.encode(createdAt, forKey: .createdAt)
try container.encode(comments, forKey: .comments)
try container.encode(bottleSizes, forKey: .bottleSizes)
var sizes = container.nestedUnkeyedContainer(
forKey: .bottleSizes)
try bottleSizes.forEach {
try sizes.encode($0.rounded())
}
{
"comments" : null,
"style" : "ipa",
"brewery_name" : "Saint Arnold",
"created_at" : "2016-05-01T12:00:00Z",
"alcohol_by_volume" : 8.8999996185302734,
"bottle_sizes" : [
12,
16
],
"name" : "Endeavor"
}
여기서 부동소수점 값이 원래 JSON 문서에서는 8.9였는데 메모리에 표현되면서 같지 않은 숫자로 되버린것은 의미없다. 만약 숫자의 정확한 표현이 필요하다면, NumberFormater로 매번 손수 포멧팅하길 원할지도 모르겠다. 특별히 통화를 다루는 API는 종종 정수형 값으로 센트 숫자를 보낸고(안전하게 반올림될 수 있) 100.0으로 나눠서 달러 값을 얻어낸다.있다
extension Beer {
init(from decoder: Decoder) throws {
}
}
let container = try decoder.container(keyedBy: CodingKeys.self)
let name = try container.decode(String.self, forKey: .name)
let abv = try container.decode(Float.self, forKey: .abv)
let brewery = try container.decode(String.self,
forKey: .brewery)
let style = try container.decode(BeerStyle.self,
forKey: .style)
let createdAt = try container.decode(Date.self,
forKey: .createdAt)
let comments = try container.decodeIfPresent(String.self,
forKey: .comments)
var bottleSizesArray = try container.nestedUnkeyedContainer(forKey: .bottleSizes)
var bottleSizes: [Float] = []
while (!bottleSizesArray.isAtEnd) {
let size = try bottleSizesArray.decode(Float.self)
bottleSizes.append(size.rounded())
}
self.init(name: name,
brewery: brewery,
abv: abv,
style: style,
createdAt: createdAt,
bottleSizes: bottleSizes,
comments: comments)
{
"name": "Lawnmower",
"info": {
"style": "kolsch",
"abv": 4.9
}
// ...
}
struct Beer : Codable {
enum CodingKeys: String, CodingKey {
case name
case brewery
case createdAt = "created_at"
case bottleSizes = "bottle_sizes"
case comments
case info // <-- NEW
}
case InfoCodingKeys: String, CodingKey {
case abv
case style
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(
keyedBy: CodingKeys.self)
var info = try encoder.nestedContainer(
keyedBy: InfoCodingKeys.self)
try info.encode(abv, forKey: .abv)
try info.encode(style, forKey: .style)
// ...
}
init(from decoder: Decoder) throws {
let container = try decoder.container(
keyedBy: CodingKeys.self)
let info = try decoder.nestedContainer(
keyedBy: InfoCodingKeys.self)
let abv = try info.decode(Float.self, forKey: .abv)
let style = try info.decode(BeerStyle.self,
forKey: .style)
// ...
}
{
"name": "Endeavor",
"brewery": "Saint Arnold",
// ...
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy:
CodingKeys.self)
try encoder.encode(brewery.name, forKey: .brewery)
// ...
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy:
CodingKeys.self)
let breweryName = try decoder.decode(String.self,
forKey: .brewery)
let brewery = Brewery(name: breweryName)
// ...
}
class Person : Codable {
var name: String?
}
class Employee : Person {
var employeeID: String?
}
let employee = Employee()
employee.employeeID = "emp123"
employee.name = "Joe"
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try! encoder.encode(employee)
print(String(data: data, encoding: .utf8)!)
{
"name" : "Joe"
}
class Person : Codable {
var name: String?
private enum CodingKeys : String, CodingKey {
case name
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
}
}
class Employee : Person {
var employeeID: String?
private enum CodingKeys : String, CodingKey {
case employeeID = "emp_id"
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(employeeID, forKey: .employeeID)
}
}
{
"emp_id" : "emp123"
}
만약 공유되는 컨테이너가 필요하면 여전히 super.encode(to: encoder)과 super.init(from: decoder)을 호출할 수 있지만, 우리는 더 안전한 컨테이너화시킨 방법을 추천한다.
try super.encode(to: container.superEncoder())
{
"super" : {
"name" : "Joe"
},
"emp_id" : "emp123"
}
enum CodingKeys : String, CodingKey {
case employeeID = "emp_id"
case person
}
override func encode(to encoder: Encoder) throws {
// ...
try super.encode(to:
container.superEncoder(forKey: .person))
}
{
"person" : {
"name" : "Joe"
},
"emp_id" : "emp123"
}
{
"customer_name": "Acme, Inc", // old key name
"migration_date": "Oct-24-1995", // different date format?
"created_at": "1991-05-12T12:00:00Z"
}
struct CustomerCodingOptions {
enum ApiVersion {
case v1
case v2
}
let apiVersion = ApiVersion.v2
let legacyDateFormatter: DateFormatter
static let key = CodingUserInfoKey(rawValue: "com.mycompany.customercodingoptions")!
}
let formatter = DateFormatter()
formatter.dateFormat = "MMM-dd-yyyy"
let options = CustomerCodingOptions(apiVersion: .v1, legacyDateFormatter: formatter)
encoder.userInfo = [ CustomerCodingOptions.key : options ]
// ...
func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self)
// here we can require this be present...
if let options = encoder.userInfo[CustomerCodingOptions.key] as? CustomerCodingOptions {
// encode the right key for the customer name
switch options.apiVersion {
case .v1:
try container.encode(name, forKey: .legacyCustomerName)
case .v2:
try container.encode(name, forKey: .name)
}
// use the provided formatter for the date
if let migrationDate = legacyMigrationDate {
let legacyDateString = options.legacyDateFormatter.string(from: migrationDate)
try container.encode(legacyDateString, forKey: .legacyMigrationDate)
}
} else {
fatalError("We require options")
}
try container.encode(createdAt, forKey: .createdAt)
}
{
"kolsh" : {
"description" : "First only brewed in Köln, Germany, now many American brewpubs..."
},
"stout" : {
"description" : "As mysterious as they look, stouts are typically dark brown to pitch black in color..."
}
}
struct BeerStyles : Codable {
struct BeerStyleKey : CodingKey {
var stringValue: String
init?(stringValue: String)? {
self.stringValue = stringValue
}
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
static let description = BeerStyleKey(stringValue: "description")!
}
struct BeerStyle : Codable {
let name: String
let description: String
}
let beerStyles : [BeerStyle]
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: BeerStyleKey.self)
var styles: [BeerStyle] = []
for key in container.allKeys {
let nested = try container.nestedContainer(keyedBy: BeerStyleKey.self,
forKey: key)
let description = try nested.decode(String.self,
forKey: .description)
styles.append(BeerStyle(name: key.stringValue,
description: description))
}
self.beerStyles = styles
}
func encode(to encoder: Encoder) throws {
var container = try encoder.container(keyedBy: BeerStyleKey.self)
for style in beerStyles {
let key = BeerStyleKey(stringValue: style.name)!
var nested = try container.nestedContainer(keyedBy: BeerStyleKey.self,
forKey: key)
try nested.encode(style.description, forKey: .description)
}
}
- DecodingError.dataCorrupted(Context): 이 데이타는 오염되었다(즉, 우리가 생각한대로 생기지 않았다). 이것은 여러분이 디코더에 제공한 data가 JSON이 전혀 아니지만, 아마도 실패한 API 콜에서 HTML 에러 페이지일 수 있다.
- DecodingError.keyNotFound(CodingKey, Context): 필요한 키가 발견되지 않았다. 이것은 질문에서 키를 보냈고 컨텍스트는 어디서, 왜 이런 일이 일어났는지에대한 정보를 제공한다. 이것을 받아다가 몇몇 키를 위한 fallback value를 적절하게 줄 수 있다.
- DecodingError.typeMismatch(Any.Type, Context): 한 타입을 기대했지만 다른것을 찾았다. 아마도 그 데이터 포멧이 첫번째 버전의 API에서 변경되었을 것이다. 이 에러를 잡고 다른 타입을 사용한 데이터 찾아볼 수 있다.
- Codable.swift: 스위프트가 오픈소스화되어서 좋은 점 중 하나는 이것이 어덯게 구현되었는지 그냥 볼 수 있다는 점이다. 꼭 한번 보자!
- Using JSON with Custom Types: 애플이 제공하는 playground 샘플인데, 더 복잡한 JSON 파싱 시나리오를 볼 수 있다.
이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은
- 블로그 댓글
- 페이스북 페이지(@나는한다번역)
- 이메일(canapio.developer@gmail.com)
- 트위터(@canapio)
으로 보내주시면 됩니다.
'Swift와 iOS > Advanced Swift' 카테고리의 다른 글
[번역]스위프트에서 동시성에대한 모든것-Part1 : 현재편 (0) | 2017.08.15 |
---|---|
[번역]문자열 보간법으로 즐겨보자 (0) | 2017.06.15 |
[번역]스위프트: UIView 애니메이션 문법 슈거 (0) | 2017.06.14 |
[번역]옵셔널 프로퍼티 (0) | 2017.06.13 |
[번역]스위프트에서 옳은 방법으로 실패 뽑아내기 (0) | 2017.06.12 |
WRITTEN BY
- tucan.dev
개인 iOS 개발, tucan9389