[iOS] Swift4 で追加された Codable で、 ?とか配列とかBoolも含んだJSONを読み書きしたい

2017/11/24

こんにちは。きんくまです。

ジッピー電卓の中で、計算履歴の保存などにJSONファイルを利用しています。

これまで使っていたライブラリがあったのですが、Swift4からはネイティブでJSONが簡単に扱えるようになったみたいなので、そちらでコードを書き直しました。(いま申請中)

Swift4のJSONについて

どういうものかは下の参考サイトを見ていただくとすぐわかります。

参考)
>> [Swift 4] SwiftyJSONを使わずにシンプルにJSONをデータ構造化する
>> Swift4 CodableでJSONが扱いやすくなる? – Qiita
>> Swift 4からFoundationに採用されるCodableプロトコルに感動した #wwdc2017
>> Swift 4でJSONの扱い – Galapagos Engineering Blog

で、ちゃんと記事を読み込んでいないせいかもしれないのですが、Optionalの? とか、 配列だとか、Boolのときにうまくできなかったので、それの対応方法を書こうと思いました。

こんな感じのJSONを読み込みたいです。

    //全部あるパターン
    let sampleJSON1 = """
{
    "name":"株式会社サンプルジェイソン",
    "address":"東京都サンプル区",
    "employees":[
        {
            "name":"田中さとし",
            "age":28,
            "is_retired":false
        },
        {
            "name":"さくらい太郎",
            "age":48,
            "is_retired":false
        },
        {
            "name":"元ぶちょう",
            "age":83,
            "is_retired":true
        },
    ]
}
"""

それで、Structを2つ用意しました。CompanyとEmployeeです。
Companyの中にemployeesというプロパティの配列が入ります。

Company.swift

import Foundation

struct Company: Codable {
    var name:String?
    var address:String?
    var employees:[Employee]?
    
    //プロパティとキーが違うときはここで変更する
    enum CodingKeys: String, CodingKey {
        case name = "name" //変更はしないけどやるときのサンプル
        case address
        case employees
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        
        //decodeIfPresentを使ってるからString に ? がつかない
        name = try values.decodeIfPresent(String.self, forKey: .name)
        
        //decodeIfPresentはこれと同じことやってるっぽい
        if values.contains(.address) {
            //String に ? がついている
            address = try values.decode(String?.self, forKey: .address)
        }
        
        employees = try values.decodeIfPresent([Employee].self, forKey: .employees)
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode(address, forKey: .address)
        try container.encode(employees, forKey: .employees)
    }
    
    func toString() -> String {
        var description = "Company ==\n"
        if let name = self.name {
            description += "name: \(name)\n"
        }
        if let address = self.address {
            description += "address: \(address)\n"
        }
        if let employees = self.employees {
            description += "employees:[ \n\n"
            for employee in employees {
                description += employee.toString(indent: "    ")
            }
            description += "]"
        }
        return description
    }
}

Employee.swift

import Foundation

struct Employee: Codable {
    var name:String?
    var age:Int?
    var isRetired:Bool?
    
    enum CodingKeys: String, CodingKey {
        case name
        case age
        case isRetired = "is_retired"
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        
        name = try values.decodeIfPresent(String.self, forKey: .name)
        age = try values.decodeIfPresent(Int.self, forKey: .age)
        isRetired = try values.decodeIfPresent(Bool.self, forKey: .isRetired)
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode(age, forKey: .age)
        try container.encode(isRetired, forKey: .isRetired)
    }
    
    func toString(indent:String?) -> String {
        let indentStr:String = indent ?? ""
        
        var description = indentStr + "Employee ==\n"
        if let name = self.name {
            description += indentStr + "name: \(name)\n"
        }
        if let age = self.age {
            description += indentStr + "age: \(age)\n"
        }
        if let isRetired = self.isRetired {
            description += indentStr + "isRetired: \(isRetired)\n"
        }
        description += "\n"
        return description
    }
}

各Structのプロパティが全てOptionalになっているのは、JSONの中に入っている時と入っていない時があるという想定です。
(実際のJSONはこういう場面が多いのではないかな?と)

toString()というのは、デバッグ用で必要ないので気にしないでください。

ポイントは、initとencodeの部分です。これはマニュアルでJSONの読み書きをしています。

Appleのドキュメントを見ると、デフォルトの型が以下のものだと、マニュアルで読み書きしなくても自動でやってくれるっぽい。
String, Int, Double, Date, Data, URL

>> Encoding and Decoding Custom Types

ただ、Boolとかはうまくいかなかったのでマニュアルで読み書きしています。

使ってみる

実際に使ってみます。使う側にこんな感じのメソッドを用意します。

    /**
     * JSONテキストからCompanyを作成
     * @param jsonText 元となるJSONテキスト
     * @return 作成されたCompany
     */
    func createCompany(from jsonText:String) -> Company? {
        guard let data = jsonText.data(using: String.Encoding.utf8) else { return nil }
        
        var company:Company?
        do{
            let decoder = JSONDecoder()
            company = try decoder.decode(Company.self, from: data)
            
        }catch let error as NSError{
            print(error.localizedDescription)
            return nil
        }
        
        return company
    }
    
    /**
     * CompanyからJSONテキストを作成
     * @param company 元になるCompany
     * @return 作成されたJSONテキスト
     */
    func createJSONText(from company:Company) -> String? {
        var jsonData:Data?
        do{
            let encoder = JSONEncoder()
            jsonData = try encoder.encode(company)
            
        }catch let error as NSError{
            print(error.localizedDescription)
            return nil
        }
        
        if jsonData == nil {
            return nil
        }
        
        let text = String(data: jsonData!, encoding: String.Encoding.utf8)
        return text
    }

使ってみます

        //読み取り
        let company1 = createCompany(from: sampleJSON1)
        if let loadedtext = company1?.toString(){
            print(loadedtext)
        }
        
        //戻してテキスト化してみる
        if company1 != nil {
            if let createdText = createJSONText(from: company1!) {
                print(createdText)
            }
        }

出力

Company ==
name: 株式会社サンプルジェイソン
address: 東京都サンプル区
employees:[ 

    Employee ==
    name: 田中さとし
    age: 28
    isRetired: false

    Employee ==
    name: さくらい太郎
    age: 48
    isRetired: false

    Employee ==
    name: 元ぶちょう
    age: 83
    isRetired: true

]


{"name":"株式会社サンプルジェイソン","address":"東京都サンプル区","employees":[{"name":"田中さとし","age":28,"is_retired":false},{"name":"さくらい太郎","age":48,"is_retired":false},{"name":"元ぶちょう","age":83,"is_retired":true}]}

うまくいってるっぽいです。

プロパティ名がJSONに入っていなかったり、値がnullになっているパターン

こういうパターンも試してみます。

もとになるJSONテキスト

    //プロパティ名がなかったり、値がnullだったりするパターン
    let sampleJSON2 = """
{
    "name":"株式会社サンプルジェイソン",
    "employees":[
        {
            "name":"おなまえ1",
            "age":null,
            "is_retired":null
        },
        {
            "name":null,
            "age":48,
            "is_retired":false
        },
        {
            "name":"元ぶちょう"
        },
    ]
}
"""

読み書きしてみます。コードはほとんど最初と同じです。

        //読み取り
        let company2 = createCompany(from: sampleJSON2)
        if let loadedtext = company2?.toString(){
            print(loadedtext)
        }
        
        //戻してテキスト化してみる
        if company2 != nil {
            if let createdText = createJSONText(from: company2!) {
                print(createdText)
            }
        }

出力

Company ==
name: 株式会社サンプルジェイソン
employees:[ 

    Employee ==
    name: おなまえ1

    Employee ==
    age: 48
    isRetired: false

    Employee ==
    name: 元ぶちょう

]


{"name":"株式会社サンプルジェイソン","address":null,"employees":[{"name":"おなまえ1","age":null,"is_retired":null},{"name":null,"age":48,"is_retired":false},{"name":"元ぶちょう","age":null,"is_retired":null}]}

このパターンもうまくいってるみたいです。

というわけで、Codableを使ったJSONの読み書きでした。ではでは。

LINEで送る
Pocket

自作iPhoneアプリ 好評発売中!
フォルメモ - シンプルなフォルダつきメモ帳
ジッピー電卓 - 消費税や割引もサクサク計算!

LINEスタンプ作りました!
毎日使える。とぼけたウサギ。LINEスタンプ販売中! 毎日使える。とぼけたウサギ

ページトップへ戻る