Codable로 JSON 다루기

원티드앱 4.0.0 버전이 출시되면서 기존에 있던 채용 공고, 회사 정보 이외에 다양한 콘텐츠, 이벤트, 테마 추천도 홈 화면에서 볼 수 있게 되었습니다. 새로운 화면을 작업하면서 Swift 4에서 추가된 Codable 프로토콜을 사용해서 JSON을 처리해봤습니다. 이제 SwiftyJSON과 같은 라이브러리를 따로 사용할 필요가 없습니다. 👋

기본적인 Codable 프로토콜 (encodable & decodable) 사용법은 애플 문서에도 이미 잘 나와 있기 때문에 실제로 적용하면서 고민했던 부분이나 도움이 된 부분만 정리해보도록 하겠습니다.

Decode type as Enum

protocol FeedItem: Codable {
    var type: FeedType { get }
}

enum FeedType: String, Codable {
    case job
    case company
    case content
    case event
    case theme
}

홈 화면에 보이는 아이템은 FeedItem 이라는 프로토콜을 준수하는 타입으로 만들고, 여기에는 FeedType 열거형에서 하나의 값을 갖게 됩니다. 이때, protocol과 enum에도 Codable을 적용하면 그대로 타입을 처리할 수 있습니다.

Custom Key Names

// user.json
{
    "login": "agiletalk",
    "avatar_url": "https://avatars2.githubusercontent.com/u/331528?v=4",
    "name": "chanju Jeon",
    "email": null,
    "public_repos": 20,
    "public_gists": 46,
    "followers": 15,
    "following": 23
}
// User.swift
struct User: Codable {
    let login: String
    var avatarUrl: String?
    var name: String
    var email: String?
    let numberOfPublicRepos: Int
    let numberOfPublicGists: Int
    let followers: Int
    let following: Int
    
    enum CodingKeys: String, CodingKey {
        case login
        case avatarUrl = "avatar_url"
        case name
        case email
        case numberOfPublicRepos = "public_repos"
        case numberOfPublicGists = "public_gists"
        case followers
        case following
    }
}

JSON을 Codable로 처리할 때, 밑줄 표기법을 낙타 표기법으로 바꾼다거나 JSON에 있는 이름과 완전히 다른 이름으로 사용하려면 CodingKey를 따로 정의하면 됩니다. (Swift 4.1부터는 밑줄 표기법과 낙타 표기법 변환은 언어 자체에서 지원한다고 합니다. 관련 링크)

Flatten out JSON

// repos.json
{
    "name": "LicensingViewController",
    "owner": {
        "login": "agiletalk",
        ...
        "type": "User"
    },
    "private": false,
    "license": {
        "key": "mit",
        "name": "MIT License",
        "url": "https://api.github.com/licenses/mit"
    }
}
// Repo.swift
struct Repo {
    let name: String
    let owner: User
    let isPrivate: Bool
    let license: String
    
    enum CodingKeys: String, CodingKey {
        case name
        case owner
        case isPrivate = "private"
        case license
    }
    
    enum LicenseKeys: String, CodingKey {
        case key
        case name
        case url
    }
}

extension Repo: Decodable {
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        name = try values.decode(String.self, forKey: .name)
        owner = try values.decode(User.self, forKey: .owner)
        isPrivate = try values.decode(Bool.self, forKey: .isPrivate)
        
        let licenseInfo = try values.nestedContainer(keyedBy: LicenseKeys.self)
        license = try licenseInfo.decode(String.self, forKey: .name)
    }
}

JSON에서 내려오는 데이터의 구조를 바꾸고 싶은 경우에는 Decodable 프로토콜에서 구현해야 하는 init(from: Decoder)를 직접 구현해서 처리할 수 있습니다. 복잡한 구조를 단순화할 때는 CodingKey를 따로 두어서 _nestedContainer_를 사용하여 처리할 수 있습니다.

Arrays, Type Matching

import Foundation

struct Repo: Decodable {
    let name: String
    let private: Bool
    let language: String
}

let json = """
[
    {
        "name": "SignUp",
        "private": false,
        "language": "Swift"
    },
    {
        "name": "tomate",
        "private": false,
        "language": "Swift"
    },
    {
        "name": "springmemo",
        "private": false,
        "language": "Python"
    }
]
""".data(using: .utf8)!

let repos = try JSONDecoder().decode([Repo].self, from: json)
repos.forEach { print($0) }

이미 Codable을 적용한 모델의 경우는 [ ]을 사용하여 바로 배열로 내려오는 데이터 구조를 처리할 수 있습니다. (위의 코드는 github API에서 일부만 사용한 예제입니다.)

var pagingArrayForType = try feeds.nestedUnkeyedContainer()
var pagingContainer = pagingArrayForType
while(!pagingArrayForType.isAtEnd) {
    let feedItem = try pagingArrayForType.nestedContainer(keyedBy: FeedKeys.self)
    let type = try feedItem.decode(FeedType.self, forKey: .type)
    switch type {
        case .job: // decode job item
        case .company: // decode company item
        case .content: // decode content item
        case .event: // decode event item
        case .theme: // decode theme item
    }
}

서로 다른 타입을 가진 배열인 경우에는 nestedUnkeyedContainer로 배열을 직접 처리할 수 있습니다. 타입을 확인하기 위해서 type만 미리 처리하고, type에 따라서 각 타입에 맞는 모델로 풀면 됩니다.


Codable 프로토콜을 적용하면서 기존에 쓰던 JSON 라이브러리를 더 이상 쓰지 않게 되었고, API 호출 이후에 JSON을 바로 Model로 변환할 수 있게 되었습니다. 그리고 JSON으로 Model을 만드는 작업을 좀 더 생각해볼 수 있었습니다.