[최종 수정일 : 2017.07.11]

원문 : https://grokswift.com/json-swift-4/

Swift 4에서 JSON 분석하기(Parsing JSON in Swift 4)

Swift 4에서 Codable을 사용해서 JSON을 생성하고 분석하는 새로운 방법이 소개되었습니다. 특히 코드에서 객체나 구조체가 웹 서비스와 통신하는데 사용되는 JONS과 비슷한 구조를 가지고 있는 경우에 일부 상용구(boilerplate)를 제거될 것입니다. 대부분의 경우 JSON을 명시적으로 분석하거나 생성하는 코드를 작성하지 않아도 되며, Swift 구조체가 JSON 구조와 정확히 일치하지 않는 경우에도 마찬가지 입니다. 이는 더 이상 toJSON()이나 init?(json: [String: Any])함수가 필요하지 않다는 의미입니다.

또한 Codable은 객체를 직렬화해서 파일에 쓰고 다시 읽기 위해 NSCoding의 사용을 대체할수 있습니다. JSON 처럼 쉽게 plist로 작업할 수 있고 다른 형식을 위해 사용자정의 인코더와 디코더를 작성 할 수 있습니다.

오늘 JSON에서 코드의 객체 또는 구조체로 변환하는 간단한 경우를 살펴봅시다. 그리고 나서 JSON이 코드에서 사용하는 객체와 구조체와 일치하지 않을때 더 복잡해 지는지 알수 있습니다. 아래에서 구조체를 사용할 것이지만, 클래스를 사용하는 경우에도 모두 동일합니다.


이 튜토리얼은 Swift 4.0을 사용합니다. Swift 4를 사용하는 가장 쉬운 방법은 XCode 9 베타를 다운로드 하는 것입니다. Xcode 8과 같이 설치 할 수 있습니다.

이전 튜토리얼에서와 같이 우리는 매우 편리한 JSONPlaceholder API를 사용할 것입니다.

JSONPlaceholder은 테스트와 프로토타입을 위한 가짜 온라인 REST API 입니다. 이미지 placeholders와 비슷하지만 웹 개발자에게 적합합니다

JSONPlaceholder은 많은 앱에서 볼수 있는 약간의 리소스와 비슷합니다 : 사용자, 게시글, 사진, 앨범, …

오늘 Todo 와 User 항목을 사용할 것입니다.

Todo의 JSON 구조체는 다음과 같습니다.

{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}

구조체로 만들어 봅시다.

struct Todo: Codable {
  var title: String
  var id: Int?
  var userId: Int
  var completed: Int
}

앱에서 Todo를 만들면 서버에 저장될때까지 id가 없도록 프로퍼티는 옵셔널 입니다.

단일 항목을 JSON으로 변환(Convert a Single Item to JSON)

Codable을 사용하기 전에, JSON을 직접 분석(parse)해야 합니다. 어떻게 동작하는지 봅시다.

JSON으로 부터 객체를 초기화로 생성해서 가져올 수 있습니다.

struct Todo {
  // ...

  init?(json: [String: Any]) {
    guard let title = json["title"] as? String,
      let id = json["id"] as? Int,
      let userId = json["userId"] as? Int,
      let completed = json["completed"] as? Int else {
        return nil
    }
    self.title = title
    self.userId = userId
    self.completed = completed
    self.id = id
  }
}

편의상, 우리가 원하는 것을 가져오기 위해 Todo의 id를 기반으로 하는 URL을 제공하는 함수를 만들어 봅시다.

struct Todo {
  // ...

  static func endpointForID(_ id: Int) -> String {
    return "https://jsonplaceholder.typicode.com/todos/\(id)"
  }
}

문제가 발생한 경우 유익한 정보를 얻을수 있도록 몇가지 오류 사례를 정의합니다.

enum BackendError: Error {
  case urlError(reason: String)
  case objectSerialization(reason: String)
}

URLSession을 사용해서 API 호출을 만들고, 에러를 확인하며, JSON을 분석하고 Todo를 반환할 수 있습니다.

static func todoByID(_ id: Int, completionHandler: @escaping (Todo?, Error?) -> Void) {
  // set up URLRequest with URL
  let endpoint = Todo.endpointForID(id)
  guard let url = URL(string: endpoint) else {
    print("Error: cannot create URL")
    let error = BackendError.urlError(reason: "Could not construct URL")
    completionHandler(nil, error)
    return
  }
  let urlRequest = URLRequest(url: url)
  
  // Make request
  let session = URLSession.shared
  let task = session.dataTask(with: urlRequest, completionHandler: {
    (data, response, error) in
    // handle response to request
    // check for error
    guard error == nil else {
      completionHandler(nil, error!)
      return
    }
    // make sure we got data in the response
    guard let responseData = data else {
      print("Error: did not receive data")
      let error = BackendError.objectSerialization(reason: "No data in response")
      completionHandler(nil, error)
      return
    }
    
    // parse the result as JSON
    // then create a Todo from the JSON
    do {
      if let todoJSON = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any],
        let todo = Todo(json: todoJSON) {
        // created a TODO object
        completionHandler(todo, nil)
      } else {
        // couldn't create a todo object from the JSON
        let error = BackendError.objectSerialization(reason: "Couldn't create a todo object from the JSON")
        completionHandler(nil, error)
      }
    } catch {
      // error trying to convert the data to JSON using JSONSerialization.jsonObject
      completionHandler(nil, error)
      return
    }
  })
  task.resume()
}

해당 함수를 호출하고 결과를 확인 하려면 :

func getTodo(_ idNumber: Int) {
  Todo.todoByIDOld(idNumber, completionHandler: { (todo, error) in
    if let error = error {
      // got an error in getting the data, need to handle it
      print(error)
      return
    }
    guard let todo = todo else {
      print("error getting first todo: result is nil")
      return
    }
    // success :)
    debugPrint(todo)
    print(todo.title)
  })
}

이제 Swift 4에서는 어떻게 될까요? 우리는 여전히 URL을 생성하고 네트워크 호출로 데이터를 얻고(오류가 없는지) 확인해야 합니다. 우리는 이 코드를 좀 더 간결하게 교체하고 JSON 기반 초기화를 제거할 수 있습니다.

do {
  if let todoJSON = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any],
    let todo = Todo(json: todoJSON) {
    // created a TODO object
    completionHandler(todo, nil)
  } else {
    // couldn't create a todo object from the JSON
    let error = BackendError.objectSerialization(reason: "Couldn't create a todo object from the JSON")
    completionHandler(nil, error)
  }
} catch {
  // error trying to convert the data to JSON using JSONSerialization.jsonObject
  completionHandler(nil, error)
  return
}

JSONSerialization.jsonObject를 대신에 초기화를 사용할 수 있습니다: let todo == Todo(json: todoJSON), 한 단계로 Codable프로토콜을 사용합니다.

먼저, Todo는 Codable을 선언해야 합니다.

struct Todo: Codable {
  // ...
}

그리고 나서 우리는 JSON 분석 코드를 변경할 수 있습니다.

static func todoByID(_ id: Int, completionHandler: @escaping (Todo?, Error?) -> Void) {
  // ...
  let task = session.dataTask(with: urlRequest, completionHandler: {
    (data, response, error) in
    guard let responseData = data else {
        // ...
    }
    guard error == nil else {
      // ...
    }
    
    let decoder = JSONDecoder()
    do {
      let todo = try decoder.decode(Todo.self, from: responseData)
      completionHandler(todo, nil)
    } catch {
      print("error trying to convert data to JSON")
      print(error)
      completionHandler(nil, error)
    }
  })
  task.resume()
}

여기에 새로운 것들이 있습니다.

let decoder = JSONDecoder()
do {
  let todo = try decoder.decode(Todo.self, from: responseData)
  completionHandler(todo, nil)
} catch {
  print("error trying to convert data to JSON")
  print(error)
  completionHandler(nil, error)
}

우리는 먼저 JSONDecoder()을 생성합니다. 그리고 나서 네트워크 호촐로부터 얻은 데이터를 디코딩 합니다. 마지막으로, 성공한 경우에 Todo의 완료 핸들러를 사용해서 호출자에게 돌려줍니다. 실패한 경우에 대신 오류를 돌려줍니다.

디코더를 사용하려면 클래스가 무엇인지 알아야 합니다. Todo.self와 분석할 데이터

let todo = try decoder.decode(Todo.self, from: responseData)

이전 버젼보다 조금 더 좋아졌습니다.

let todoJSON = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any]
let todo = Todo(json: todoJSON)

또한 JSON으로 부터 Todo를 생성하는 초기화를 작성할 필요가 없다는 것을 의미합니다. JSON으로 부터 모든 요소를 추출하고 Todo의 프로퍼티에 설정하는 것이 완료 되었습니다. 이제 우리는 init?(json: [String: Any])함수를 사용할 수 있습니다.

배열의 항목들을 JSON으로 변환(Convert an Array of Items to JSON)

마찬가지로, 우리는 https://jsonplaceholder.typicode.com/todos로 부터 Todo를 쉽게 가져올 수 있습니다.

우리가 참조할 수 있도록 URL 문자열을 추가하세요:

struct Todo: Codable {
  // ...
  static func endpointForTodos() -> String {
    return "https://jsonplaceholder.typicode.com/todos/"
  }
}

그리고 todoById(_:)와 비슷한 새로운 함수를 생성합니다.

static func allTodos(completionHandler: @escaping ([Todo]?, Error?) -> Void) {
  let endpoint = Todo.endpointForTodos()
  guard let url = URL(string: endpoint) else {
    print("Error: cannot create URL")
    let error = BackendError.urlError(reason: "Could not construct URL")
    completionHandler(nil, error)
    return
  }
  let urlRequest = URLRequest(url: url)
  
  let session = URLSession.shared
  
  let task = session.dataTask(with: urlRequest) {
    (data, response, error) in
    guard let responseData = data else {
      print("Error: did not receive data")
      completionHandler(nil, error)
      return
    }
    guard error == nil else {
      completionHandler(nil, error)
      return
    }
    
    let decoder = JSONDecoder()
    do {
      let todos = try decoder.decode([Todo].self, from: responseData)
      completionHandler(todos, nil)
    } catch {
      print("error trying to convert data to JSON")
      print(error)
      completionHandler(nil, error)
    }
  }
  task.resume()
}

JSON 처리는 이전 함수와 거의 비슷합니다.

let decoder = JSONDecoder()
do {
  let todos = try decoder.decode([Todo].self, from: responseData)
  completionHandler(todos, nil)
} catch {
  print("error trying to convert data to JSON")
  print(error)
  completionHandler(nil, error)
}

차이점은 decoder.decode에 전달되는 타입입니다. 하나의 Todo의 인스턴스 대신에 Todos의 배열입니다: [Todo].self

해당 함수 호출하는 것은 완료 핸들러에서 하나가 아닌 Todo 항목들의 배열을 가져오는 것을 제외하면 매우 비슷합니다.

func getAllTodos() {
  Todo.allTodos { (todos, error) in
    if let error = error {
      // got an error in getting the data
      print(error)
      return
    }
    guard let todos = todos else {
      print("error getting all todos: result is nil")
      return
    }
    // success :)
    debugPrint(todos)
    print(todos.count)
  }
}

그것은 완료 핸들러가 completiionHandler: @escaping ([Todo]?, Error?) -> Void 와 같이 선언되었기 때문입니다.

항목으로부터 JSON 생성(Generate JSON from an Item)

Codable프로토콜은 두개의 프로토콜로 구성됩니다: Encodable, Decodable. 지금까지 Decodable을 사용해서 JSON을 분석하였습니다. Encodable은 JSON도 쉽게 만들수 있습니다.

다음은 Codable를 사용하지 않고 웹 서비스에 Todo를 저장하는 예제입니다.

func save(completionHandler: @escaping (Error?) -> Void) {
  let todosEndpoint = Todo.endpointForTodos()
  guard let todosURL = URL(string: todosEndpoint) else {
    let error = BackendError.urlError(reason: "Could not create URL")
    completionHandler(error)
    return
  }
  var todosUrlRequest = URLRequest(url: todosURL)
  todosUrlRequest.httpMethod = "POST"
  
  let newTodoAsJSON = self.toJSON()
  
  do {
    let jsonTodo = try JSONSerialization.data(withJSONObject: newTodoAsJSON, options: [])
    todosUrlRequest.httpBody = jsonTodo
  } catch {
    let error = BackendError.objectSerialization(reason: "Could not create JSON from Todo")
    completionHandler(error)
    return
  }
  
  let session = URLSession.shared
  
  let task = session.dataTask(with: todosUrlRequest, completionHandler: {
    (data, response, error) in
    guard error == nil else {
      completionHandler(error!)
      return
    }
    
    // if we didn't get an error then it succeeded
    completionHandler(nil)
  })
  task.resume()
}

이 작업을 하려면 Todo를 JSON으로 변환하는 함수를 추가해야 합니다.

struct Todo {
  // ...
  func toJSON() -> [String: Any] {
    var json = [String: Any]()
    json["title"] = title
    json["userId"] = userId
    json["completed"] = completed
    if let id = id {
      json["id"] = id
    }
    return json
  }
}

Swift 4에서는 JSONSerialization 대신 JSONEncoder를 사용해서 toJSON() 같은 함수를 작성하는 것을 피할 수 있습니다.

func save(completionHandler: @escaping (Error?) -> Void) {
  // ...
  
  var todosUrlRequest = URLRequest(url: todosURL)
  todosUrlRequest.httpMethod = "POST"
  
  let encoder = JSONEncoder()
  do {
    let newTodoAsJSON = try encoder.encode(self)
    todosUrlRequest.httpBody = newTodoAsJSON
  } catch {
    print(error)
    completionHandler(error)
  }
  
  // ...
  
  let task = session.dataTask(with: todosUrlRequest, completionHandler: {
    (data, response, error) in
    // ...
  })
  task.resume()
}

옛날 버젼은 더이상 호출하지 않습니다.

let newTodoAsJSON = try JSONSerialization.data(withJSONObject: newTodoAsJSON, options: [])

비교 대상:

let newTodoAsJSON = try encoder.encode(self)

하지만 toJSON() 함수를 삭제 할수 있는 것이 좋습니다.

중첩된 JSON 객체들(Nested JSON Objects)

JSON 객체 내부에 객체가 포함되어 있다면 어떨까요? 예를들어, JSON Placeholder API로 부터 User에 주소와 회사가 있습니다.

{
  "id": 1,
  "name": "Leanne Graham",
  "username": "Bret",
  "email": "Sincere@april.biz",
  "address": {
    "street": "Kulas Light",
    "suite": "Apt. 556",
    "city": "Gwenborough",
    "zipcode": "92998-3874",
    "geo": {
      "lat": "-37.3159",
      "lng": "81.1496"
    }
  },
  "phone": "1-770-736-8031 x56442",
  "website": "hildegard.org",
  "company": {
    "name": "Romaguera-Crona",
    "catchPhrase": "Multi-layered client-server neural-net",
    "bs": "harness real-time e-markets"
  }
}

사용자 필드로 부터 몇개의 필드를 분석하고 주소 객체 내부의 몇개를 분석해 봅시다. 먼저 User과 Address에 대해 모두 Codable 구조체를 만듭니다:

struct Address: Codable {
  let city: String
  let zipcode: String
}

struct User: Codable {
  let id: Int?
  let name: String
  let email: String
  let address: Address
  
  // MARK: URLs
  static func endpointForID(_ id: Int) -> String {
    return "https://jsonplaceholder.typicode.com/users/\(id)"
  }
}

그리고나서 사용자를 얻는 함수를 설정합니다.

static func userByID(_ id: Int, completionHandler: @escaping (User?, Error?) -> Void) {
  let endpoint = User.endpointForID(id)
  guard let url = URL(string: endpoint) else {
    print("Error: cannot create URL")
    let error = BackendError.urlError(reason: "Could not create URL")
    completionHandler(nil, error)
    return
  }
  let urlRequest = URLRequest(url: url)
  
  let session = URLSession.shared
  
  let task = session.dataTask(with: urlRequest, completionHandler: {
    (data, response, error) in
    guard let responseData = data else {
      print("Error: did not receive data")
      completionHandler(nil, error)
      return
    }
    guard error == nil else {
      completionHandler(nil, error!)
      return
    }
    
    let decoder = JSONDecoder()
    do {
      let user = try decoder.decode(User.self, from: responseData)
      completionHandler(user, nil)
    } catch {
      print("error trying to convert data to JSON")
      print(error)
      completionHandler(nil, error)
    }
  })
  task.resume()
}

JSON 인코딩을 수행하는 부분은 다음과 같습니다:

let user = try decoder.decode(User.self, from: responseData)

그리고나서 그 호출하고 사용자와 주소를 출력할 수 있습니다.

func getUser(_ idNumber: Int) {
  User.userByID(idNumber, completionHandler: { (user, error) in
    if let error = error {
      // got an error in getting the data, need to handle it
      print("error calling POST on /todos")
      print(error)
      return
    }
    guard let user = user else {
      print("error getting user: result is nil")
      return
    }
    // success :)
    debugPrint(user)
    print(user.name)
    print(user.address.city)
  })
}

그것은 동작합니다! 다음은 출력된 내용입니다.

grok101.User(id: Optional(1), name: "Leanne Graham", email: "Sincere@april.biz", address: grok101.Address(city: "Gwenborough", zipcode: "92998-3874"))
Leanne Graham
Gwenborough

이 예제에서 두가지를 보여줍니다.

  1. 중첩된 JSON 객체들을 분석하려면 중첩된 JSON 객체들에 대해 Codable로 된 프로퍼티를 만들면 됩니다.
  2. 우리는 JSON에서 모든 요소를 분석할 필요가 없습니다. 우리는 Codable 항목에 대한 프로퍼티만을 생성해서 분석해서 원하는 요소들을 선택하고 가져올 수 있습니다.

JSON이 나의 구조체/클래스와 일치하지 않는 경우는 무엇일까요?

JSON이 객체와 일치할때 Codable은 거의 마술처럼 동작합니다. 가끔씩 JSON이 Swift 선언과 정확히 일치하지 않는 경우가 있습니다. 어떻게 처리할 수 있나요?

다른 프로퍼티 이름(Different Property Names)

가장 간단한 경우는 프로퍼티 이름이 일치하지 않는 경우입니다. serverId와 같은 id 프로퍼티에 대한 좀 더 많은(verbose) 이름을 사용한다고 가정해 봅니다.

struct Todo: Codable {
  var title: String
  var serverId: Int?
  var userId: Int
  var completed: Int
}

이제 todoById(:_) 함수를 실행하면 그것은 동작하지만 serverId값 없이 끝날 것입니다.

Todo(title: "delectus aut autem", serverId: nil, userId: 1, completed: 0)

serverId는 옵셔널이라 분석이 실패하지는 않지만, 올바르지 않을 것입니다. 운이 좋게도 Codable프로토콜을 사용하여 enum CodingKeys를 사용하는 각 프로퍼티와 일치해야 하는 json 키를 지정할 수 있습니다.

enum CodingKeys: String, CodingKey {
  case title
  case serverId = "id"
  case userId
  case completed
}

CodingKeys에는 JONS에서 분석할 모든 키를 포함합니다. 프로퍼티의 이름이 json 키와 일치하는 경우 case propertyName만 포함하면 됩니다. 일치 하지 않는 경우 프로퍼티 이름과 json 키를 모두 포함해야 합니다: case propertyName = json_key. 모두 일치한 경우 CodingKeys의 기본 버젼은 자동으로 생성되고 코드에 추가할 필요가 없습니다.

case serverId = id를 사용해서 JSON 분석을 고쳐서 serverId가 올바르게 분석됩니다.

Todo(title: "delectus aut autem", serverId: Optional(1), userId: 1, completed: 0)

이름이 JSON과 일치하지 않는 프로퍼티가 옵셔널이 아닌 경우에 어떻게 하나요? 옵셔널이 아닌 프로퍼티 이름중 하나를 변경해 봅시다.

struct Todo: Codable {
  var displayTitle: String
  var serverId: Int?
  var userId: Int
  var completed: Int
}

title을 displayTitle으로 변경했습니다.

codingKeys를 사용하지 않은 경우 try decoder.decode(Todo.self, from: responseData)를 호출할때 오류가 발생할 것입니다.

keyNotFound(grok101.Todo.(CodingKeys in _6B02657B511E8A9934EF1ACB6A92D55E).displayTitle,
Swift.DecodingError.Context(codingPath: [Optional(grok101.Todo.(CodingKeys in _6B02657B511E8A9934EF1ACB6A92D55E).displayTitle)],
debugDescription: "Key not found when expecting non-optional type String for coding key \"displayTitle\""))

옵셔널이 아닌 displayTitle프로퍼티와 일치하는 JSON 키를 찾을수 없다는 것을 말해줍니다.

이 문제를 해결하기 위해 CodingKeys를 사용해 보세요.

enum CodingKeys: String, CodingKey {
  case displayTitle = "title"
  case serverId = "id"
  case userId
  case completed
}

성공했습니다!

Todo(displayTitle: "delectus aut autem", serverId: Optional(1), userId: 1, completed: 0)

동일한 codingKeys은 모든 Todo 항목을 가져오는 함수와 함께 동작하고 새로운 Todo를 저장하려면 API 호출을 지원하기 위해 다른 것을 변경할 필요가 없습니다.

다른 구조체 분석하기(Parsing a Different Structure)

Swift 코드에서 원하는 항목이 JSON과 다른 구조로 되어 있는 경우가 있습니다. 원하는 몇개의 프로퍼티를 얻기 위해 JSON의 일부를 파헤쳐야 할 수도 있거나 JSON의 일부 요소들 사이에서 분리된 항목들을 가질 수도 있습니다.

새로운 Todo를 생성할때 우리가 얻은 결과를 무시했습니다. 이것은 다음과 같습니다.

{
  "{\"title\":\"First todo\",\"userId\":1,\"completed\":0}": "",
  "id": 201
}

JSON에는 두개의 요소가 있습니다: 우리가 보낸것(이상한 키처럼)과 새롭게 생성된 Todo에 대한 id. 우리가 정말로 원하는 것은 우리의 Todo에 추가할 수 있는 새로운 id입니다. 먼저 Todo 분석하고 예상하는 함수를 업데이트 합니다.

호출하는 함수에 대해, 완료 핸들러에 두번째 매개변수를 추가할 것입니다.

func saveNewTodo() {
  let newTodo = Todo(displayTitle: "First todo", serverId: nil, userId: 1, completed: 0)
  newTodo.save { (todo, error) in
    if let error = error {
      // got an error in getting the data, need to handle it
      print("error calling POST on /todos")
      print(error)
      return
    }
    guard let todo = todo else {
      print("did not get todo in response from creating todo")
      return
    }
    // success!
    print("Saved new todo")
    debugPrint(todo)
  }
}

save()함수의 경우에, 두번째 매개변수를 완료 핸들러에 추가해야 합니다.

func save(completionHandler: @escaping (Todo?, Error?) -> Void)

두개의 매개변수를 사용해서 완료 핸들러에 대한 모든 호출을 업데이트합니다.

func save(completionHandler: @escaping (Todo?, Error?) -> Void) {
  let todosEndpoint = Todo.endpointForTodos()
  guard let todosURL = URL(string: todosEndpoint) else {
    let error = BackendError.urlError(reason: "Could not construct URL")
    completionHandler(nil, error)
    return
  }
  var todosUrlRequest = URLRequest(url: todosURL)
  todosUrlRequest.httpMethod = "POST"
  
  let encoder = JSONEncoder()
  do {
    let newTodoAsJSON = try encoder.encode(self)
    todosUrlRequest.httpBody = newTodoAsJSON
    // See if it's right
    if let bodyData = todosUrlRequest.httpBody {
      print(String(data: bodyData, encoding: .utf8) ?? "no body data")
    }
  } catch {
    print(error)
    completionHandler(nil, error)
  }
  
  let session = URLSession.shared
  
  let task = session.dataTask(with: todosUrlRequest, completionHandler: {
    (data, response, error) in
    guard error == nil else {
      completionHandler(nil, error!)
      return
    }
    // TODO: parse response
    completionHandler(nil, nil)
  })
  task.resume()
}

그리고 응답을 분석하기 위해 시도합니다:

let task = session.dataTask(with: todosUrlRequest, completionHandler: {
  (data, response, error) in
  guard error == nil else {
    let error = error!
    completionHandler(nil, error)
    return
  }
  guard let responseData = data else {
    let error = BackendError.objectSerialization(reason: "No data in response")
    completionHandler(nil, error)
    return
  }
  
  let decoder = JSONDecoder()
  do {
    let todo = try decoder.decode(Todo.self, from: responseData)
    completionHandler(todo, nil)
  } catch {
    print("error parsing response from POST on /todos")
    print(error)
    completionHandler(nil, error)
  }
})
task.resume()

하지만 JSON이 Todo에 대해 기대하는 형식이 아니기 때문에 동작하지 않습니다.

keyNotFound(grok101.Todo.CodingKeys.displayTitle,
Swift.DecodingError.Context(codingPath: [Optional(grok101.Todo.CodingKeys.displayTitle)],
debugDescription: "Key not found when expecting non-optional type String for coding key \"displayTitle\""))

디코더에게 어떻게 JSON을 분석하도록 할수 있나요? 가장 간단한 방법은 JSON을 미러링(mirrors)하는 새로운 타입을 만들고 나서 그 항목으로부터 Todo를 생성하는 것입니다.

JSON으로부터 새로운 구조체를 정의하기 위해 id만 필요하기 때문에 Todo를 생성한 응답을 캡슐화 합니다.

struct CreateTodoResult: Codable {
  var id: Int?
}

우리는 CreateTodoResult를 분석하고 지정하고 나서 id를 가져오고 반환하기 전에 Todo에 추가합니다.

let decoder = JSONDecoder()
do {
  let createResult = try decoder.decode(CreateTodoResult.self, from: responseData)
  var todo = self
  todo.serverId = createResult.id
  completionHandler(todo, nil)
} catch {
  print("error parsing response from POST on /todos")
  print(error)
  completionHandler(nil, error)
}

JSON과 일치해서 작업하는 구조체를 쉽게 생성할수 없는 경우, decode함수를 오버라이드(override) 할 수 있습니다. 예를 들어, 사용자의 주소가 중첩된 구조체의 일부가 되는 것을 원하지 않는 경우:

struct User: Codable {
  let id: Int?
  let name: String
  let email: String
  let city: String
  let zipcode: String

  // ...
}

JSON(우리 구조체에서 그렇지 않음)에서 중첩된 구조체를 처리하기 위해, 우리는 address에 대해 포함하는 CodingKeys를 정의합니다.
그리고나서 CodingKeys와 유사한 AddressKeys라는 다른 열거형을 추가하고 Address JSON 객체의 내부에서 분석하길 원하는 키를 가지고 있습니다.

struct User: Decodable {
  // ...
  
  enum CodingKeys: String, CodingKey {
    case id
    case name
    case email
    case address
  }
  
  enum AddressKeys: String, CodingKey {
    case city
    case zipcode
  }
 
  // ...
}

이제 우리는 JSON으로 부터 User을 생성할때 , 디코더에서 사용하는 초기화를 오버라이드 할 수 있습니다. 우리 구조체의 프로퍼티에 JSON으로 부터 값을 매핑하는 자체 코드를 작성할 수 있습니다. 기본 멤버단위(memberwise) 초기화를 유지하기 위해 확장에 초기화를 위치시킵니다(그렇지 않으면 자체함수를 작성하지 않고 self.init(displayTitle: title, serverId: serverId, userId: userId, completed: completed) 호출할 수 없습니다.).

extension User {
  public init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    let id = try values.decodeIfPresent(Int.self, forKey: .id)
    let name = try values.decode(String.self, forKey: .name)
    let email = try values.decode(String.self, forKey: .email)
    
    let addressValues = try values.nestedContainer(keyedBy: AddressKeys.self, forKey: .address)
    let city = try addressValues.decode(String.self, forKey: .city)
    let zipcode = try addressValues.decode(String.self, forKey: .zipcode)
    
    self.init(id: id, name: name, email: email, city: city, zipcode: zipcode)
  }
}

decoder.container(keyedBy: CodingKeys.self)는 JSON을 살펴보고 CodingKeys를 사용해서 각 요소들을 가져올 것입니다. 그리고 나서 값을 추출하기 위해 각 프로퍼티에 대해 values.decodeIfPresent(Int.self, forKey: .id)을 수행합니다. 키에 대한 열거형은 CodingKeys을 기반으로 하며 각 프로퍼티에 대해 하나를 가질 것입니다.

address 키 내의 요소의 경우, 우리는 그 키를 기반으로 컨테이너를(container) 가져와야 합니다 : values.nestedContainer(keyedBy: AddressKeys.self, forKey: .address). 그리고 나서 addressValues.decode(String.self, forKey: .city) 으로 추출할 수 있습니다

다른 구조체 보내기(Sending a Different Structure)

다른 구조체를 보내기 위해 encode함수의 사용자 정의 버젼을 구현 할수 있습니다. 예를 들어, PATCH 요청을 사용해서 업데이트 하기 위해 매개변수 하나를 보내면 됩니다. 사용자의 이메일 주소를 업데이트하려고 가정합니다.

선언을 let에서 var로 변경할 수 있습니다.

struct User: Codable {
  let id: Int?
  let name: String
  var email: String
  let city: String
  let zipcode: String
  // ...
}

그리고 확장에 encode함수를 추가하세요. 다음은 이메일 매개변수만 포함하는 방법입니다.

extension User {
  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(email, forKey: .email)
  }
}

그리고 나서 사용자를 가지고, 로컬에서 이메일 주소를 업데이트하고 서버로 보낼수 있습니다.

var editedUser = user
editedUser.email = "a@aol.com"
editedUser.update() {
  error in
  print(error ?? "no error")
}

그리고 PATCH 요청을 보내기 위해 update()함수를 생성합니다.

func update(completionHandler: @escaping (Error?) -> Void) {
  guard let id = self.id else {
    let error = BackendError.urlError(reason: "No ID provided for PATCH")
    completionHandler(error)
    return
  }
  let endpoint = User.endpointForID(id)
  guard let url = URL(string: endpoint) else {
    let error = BackendError.urlError(reason: "Could not construct URL")
    completionHandler(error)
    return
  }
  var urlRequest = URLRequest(url: url)
  urlRequest.httpMethod = "PATCH"
  var headers = urlRequest.allHTTPHeaderFields ?? [:]
  headers["Content-Type"] = "application/json"
  urlRequest.allHTTPHeaderFields = headers
  
  let encoder = JSONEncoder()
  do {
    let asJSON = try encoder.encode(self)
    urlRequest.httpBody = asJSON
    // See if it's right
    if let bodyData = urlRequest.httpBody {
      print(String(data: bodyData, encoding: .utf8) ?? "no body data")
    }
  } catch {
    print(error)
    completionHandler(error)
  }
  
  let session = URLSession.shared
  
  let task = session.dataTask(with: urlRequest) {
    (data, response, error) in
    guard error == nil else {
      let error = error!
      completionHandler(error)
      return
    }
    guard let responseData = data else {
      let error = BackendError.objectSerialization(reason: "No data in response")
      completionHandler(error)
      return
    }
    
    print(String(data: responseData, encoding: .utf8) ?? "No response data as string")
    completionHandler(nil)
  }
  task.resume()
}

HTTP 메소드와 헤더에 Content-Type: aaplication/json을 추가하는 것을 제외하면 save()함수와 거의 같습니다.

urlRequest.httpMethod = "PATCH"

var headers = urlRequest.allHTTPHeaderFields ?? [:] // create empty dictionary if headers is nil
headers["Content-Type"] = "application/json"
urlRequest.allHTTPHeaderFields = headers

요청을 보내기 전에 본문이 올바르게 설정되어 있는지 확인할 수 있습니다.

if let bodyData = urlRequest.httpBody {
  print(String(data: bodyData, encoding: .utf8) ?? "no body data")
}

우리가 예상한 것이 출력됩니다.

{"email":"a@aol.com"}

그리고 네트워크 호출이 완료된후에, 우리는 이메일 주소가 변경되었는지 확인 하기 위해 응답 본문을 출력 할 수 있습니다.

guard let responseData = data else {
  let error = BackendError.objectSerialization(reason: "No data in response")
  completionHandler(error)
  return
}

print(String(data: responseData, encoding: .utf8) ?? "No response data as string")

다음과 같이 출력됩니다

{
  "id": 1,
  "name": "Leanne Graham",
  "username": "Bret",
  "email": "a@aol.com",
  // ...
}

그리고 이메일 주소가 성공적으로 변경되었습니다.

주의사항 몇가지(Some Warnings)

JSON 분석은 프로퍼티의 이름에 따라 다르기 때문에 이름을 변경하지 않도록 합니다(JSON이 변경되지 않는 한). 프로퍼티 이름을 바꿔서 JSON 인코딩/디코딩을 중단하는 경우에 대해 테스트를 작성하는 것이 좋습니다.

사용자정의 디코딩 또는 인코딩을 위해 init(from decoder: Decoder) 또는 encode(to encoder: Encoder) 를 정의하는 경우 해당 클래스의 모든 인코딩과 디코딩에 사용됩니다. 예를 들어,

새로운 사용자를 생성하기 위해 새 함수를 만드는 경우, User를 위한 이메일 전용 인코딩이 사용되며, 아마도 우리가 원하지 않은 것입니다.

Swift 4에서 JSON 분석에 대한 모든것(And That’s All for Parsing JSON in Swift 4 Today)

Swift 4에서 Codable을 사용하면 JSON 분석을 간단하고 명확해 집니다. 코드와 관련해서 JSON에서 키와 구조체와의 차이(discrepancies)를 처리할 만큼 유연합니다. 따라서 Swift 코드에서 jsonkeywith_underscores와 같은 변수 이름을 사용할 필요가 없습니다.

이 주제에 대해서 자세한 정보는 Apple에서 새로 배포된 문서를 참고합니다.

샘플코드(Playground): 사용자정의 타입으로 JSON 사용하기(Using JSON with Custom Types)

사용자 정의 타입 인코딩과 디코딩 하기(Encoding and Decoding Custom Types)

저작자 표시
신고
Posted by 까칠코더 까칠코더


티스토리 툴바