[Swift] JSON データの扱い方

Swift

Swift での JSON データの扱い方を説明します

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Monterery beta 2
  • Xcode 13 beta2

JSON ファイル

JSON とは、JavaScript Object Notation の省略形です。和訳すると「JavaScript のオブジェクト記法」でしょうか。

名前に JavaScript と入っていますが、JavaScript 環境だけではなく、データを受け渡す時のフォーマットとして 広く使われるようになっています。

JSON ファイル例


{
  "name": "Suzuki",
  "age": 30
}

特に説明がなくとも、なんとなく意味がとれるのも、JSON ファイルの特徴です。

表されているデータは、"name" という名前のプロパティを "Suzuki" という値で持ち、"age" という名前のプロパティを 30 という値で持つことを表しています。

MEMO
厳密には、プロパティの型情報は含まれていません。

対応する Swift struct

Swift 的に書くと以下のような struct とそのインスタンスに該当します。


struct Person {
  var name: String
  var age: Int
}

let Person = Person(name: "Suzuki", agen: 30)

ファイルデータの扱い

Swift では、ファイルのデータを Data として扱い、処理することがよくあります。

ファイルの中身が文字列の時は、一度バイナリデータを表す Data として受け取り、そこから String として復元します。

例えば、FileWrapper にはそのファイルの中身を取得する regularFileContents メソッドが用意されています。このメソッドは、Data を返します。


var regularFileContents: Data? { get }

逆に 文字列データをファイルに書き出す時向けに、String を Data に変換するためのメソッドが StringProtocol に用意されています。


func data(using encoding: String.Encoding, allowLossyConversion: Bool = false) -> Data?

ファイル処理には、FileWrapper を使いますが、Data を使って、FileWrapper を作成する initializer も用意されています。この FileWrapper をつかうことで、指定した中身を持つファイルを作ることができます。


init(regularFileWithContents contents: Data)

以下では、具体的なファイルの扱いを省略して、Data として持っている JSON を扱っていきます。
つまり、JSON ファイルから読み込んだと想定する Data から、Swift の struct/class を復元したり、struct/class を (JSON ファイルの中身になるはずの) Data へと変換します。

以下は、先の JSON データを Data にするためのコードになります。最後に付与されている ".data(using: .utf8)" が文字列を Data 化している箇所です。


static let localJSONData = """
{
    "name": "Suzuki",
    "age": 30
}
""".data(using: .utf8)!
}

JSON を処理するために使用するクラス、プロトコル

JSONEncoder, JSONDecoder

データを JSON に/から 変換してくれるクラスとして JSONEncoder, JSONDecoder が提供されています。

Apple のドキュメントは、こちらこちら

JSONEncoder, JSONDecoder が対象とするデータには、Codable であることが必要という条件があります。

Codable, Encodable, Decodable

Apple のドキュメントは、こちらこちらこちら

ドキュメントを見るとわかりますが、Codable = Encodable + Decodable です。

Encodable, Decodable の名称は、それぞれ "Encode + able(できる)" と ”Decode + able(できる)" からきています。

encode は、ある特定のルールに従って、データを目的のフォーマットに変換すること。decode とは特定のフォーマットからデータを復元することです。

これらを使用して、処理していきます。

Swift の多くのタイプは、すでに Codable

Int, Double, String 等は、すでに Codable に conform しています。

ですので、Int を JSON へ保存したり、JSON から Int を復元したりすることは、簡単にできます。

ですが、通常 JSON を使って処理したい対象は、自分で定義した (もう少し複雑な構造を持つ) class や struct でしょう。

自分で定義した class/struct を Codable に conform させる

Swift で定義した class/struct は、その class/struct が保持するプロパティがすべて Codable であれば、Codable にすることができます。

Codable にする方法は、その class/struct の定義で Codable に conform していると宣言することです。

以下の Person という struct は、プロパティに String, Int を持っていますが、どちらも Codable ですので、Person も宣言に追加することで Codable にすることができます。


struct Person:Codable {  // <- Codable に conform
    var name: String
    var age: Int
}

JSON データの読み書き:シンプルなケース

JSON からの読み込み

JSONDecoder.decode を使用して、JSON を Swift の class/struct に変換します。


let localData = """
{
    "name": "Suzuki",
    "age": 30
}
""".data(using: .utf8)!
if let personFromJSON = try? JSONDecoder().decode(Person.self, from: localData) {
    
}

JSON への書き出し

JSONEncoder.encode を使用して、Swift の class/struct を JSON の Data に変換します。


let person = Person(name: "Takahashi", age: 22)
if let data = try? JSONEncoder().encode(person) {
    ...
}

JSON データの読み書き:異なるプロパティ名の JSON を処理したい

JSON ファイルを外部とのやりとりに使うと、class/struct のプロパティ名と一致しないケースが発生することがあります。

そのような時には、CodingKeys を定義することで、対応関係を指定することができます。

JSON フォーマット


{
    "name": "Suzuki",
    "age": 30
}

Swift struct


class Person:Codable {
    var namae: String
    var age: Int

    init(name: String, age: Int) {
        self.namae = name
        self.age = age
    }
}

JSON で "name" というキー値になっているものを Swift では、"namae" で扱いたいケースです。

読み書きいずれも、CodingKeys を class/struct で定義することでその対応関係を指定することができます。

なお、CodingKeys を定義したときは、JSON での処理対象のプロパティすべてを定義する必要があります。


class Person:Codable {
    var namae: String
    var age: Int

    private enum CodingKeys: String, CodingKey{
        // (1)
        case namae = "name"
        // (2)
        case age
    }

    init(name: String, age: Int) {
        self.namae = name
        self.age = age
    }
}
コード解説
  1. ここで、プロパティ namae が JSON での name に対応することを定義します
  2. age は、そのままの処理なので、処理対象であることを定義するだけです

JSON からの読み込み

CodingKeys が正しく定義されていれば、これまで通りの読み込みで処理できます。

JSON への書き出し

書き出し処理も同じです。CodingKeys が正しく定義されていれば、これまで通りのコードで処理されます。

JSON データの読み書き:特定のプロパティを対象から外したい

データが複雑になってくると、class/struct の一部プロパティのみを JSON とやりとりしたいケースが発生することがあります。

先ほどの CodingKeys を適切に定義することで、一部のプロパティのみを JSON とのやりとりの対象にすることができます。

以下は、JSON から age 情報を取得しないようにするケースです。


class Person:Codable {
    var name: String
    // (1)
    var age: Int = 10

    // (2)
    private enum CodingKeys: String, CodingKey{
        case name
//        case age
    }

    init(name: String, age: Int = 10) {
        self.name = name
        self.age = age
    }
}
コード解説
  1. JSON から読み込まない情報は、デフォルト値が設定される必要があります(インスタンス生成時に不定値をとることは許されません)
  2. CodingKeys から age を外すことで、JSONEncoder,JSONDecoder の処理対象から外れます

JSON からの読み込み

CodingKeys が正しく定義されていれば、これまで通りの読み込みで処理できます。

JSON への書き出し

書き出し処理も同じです。CodingKeys が正しく定義されていれば、これまで通りのコードで処理されます。

処理対象になっていないプロパティは、デフォルト値が適用されていることに注意する必要があります。

JSON データの読み書き:特殊な変換を実装指定

さらに複雑なケースを想定すると、JSON と Swift のモデルの関係が1:1ではない時があります。つまり、何らかの変換をして JSON を読み込みたい/書き出したいケースです。

このような時には、Codable に準拠することで提供されていたデフォルト実装を上書きすることになります。

JSON からの読み込み

Decodable から提供されるデフォルト実装を上書きすることになります。

Decodable から提供される実装

Person を Codable に conform させることで以下のようなコードがデフォルト実装として提供されます。


    init(from decoder: Decoder) throws {
        let myContainer = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try myContainer.decode(String.self, forKey: .name)
        self.age = try myContainer.decode(Int.self, forKey: .age)
    }

ここで、name をすべて小文字にして読み込みたいとすると、以下のような変換を init(from decoder:) で行うことになります。


    init(from decoder: Decoder) throws {
        let myContainer = try decoder.container(keyedBy: CodingKeys.self)
        let jsonName = try myContainer.decode(String.self, forKey: .name)
        self.name = jsonName.lowercased()
        self.age = try myContainer.decode(Int.self, forKey: .age)
    }

JSON への書き出し

Encodable から提供されるデフォルト実装を上書きすることになります。

Encodable から提供される実装

Person を Codable に conform させることで以下のようなコードがデフォルト実装として提供されます。


    func encode(to encoder: Encoder) throws {
        try name.encode(to: encoder)
        try age.encode(to: encoder)
    }

ここで、例えば name をすべて大文字にして JSON に保存したいとすると以下のような変換を行うことになります。


    func encode(to encoder: Encoder) throws {
        let uppercasedName = name.uppercased()
        try uppercasedName.encode(to: encoder)
        try age.encode(to: encoder)
    }

この方法を使うと、ここで説明している変換だけではなく、細かい変換ルールを制御することができます。

まとめ:Swift での JSON データの扱い方

Swift での JSON データの扱い方
  • Codable = Encodable + Decodable がポイントとなるプロトコル
  • 多くの基本型が Codable 対応している
  • Codable な型のプロパティを持つ class/struct は、Codable になる
  • CodingKeys を使うことで、キー値のカスタマイズが可能
  • encode(to encoder:) と init(from decoder:) を使用することで、こまかなカスタマイズが可能

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です