【Swift】JSONをカスタム構造体に変換する方法

この記事でわかること

  • JSON ⇆ Swiftカスタム構造体間のデコード・エンコード方法
  • コーディングキーの利用方法
  • カスタムデコード・エンコードの実装方法

動作確認済みの環境

【XCode】15.3
【Swift】5.10
【iOS】17.4.1
【macOS】Sonoma 14.4.1

当ブログでは記事内で紹介する参考文献にアフィリエイトリンクを付与する場合がございます。それらはすべて著者が実際に利用している文献ですので、安心して参照いただけます。

スポンサーリンク
目次

デコード・エンコードの基礎知識

あるデータを一定の規則に基づいて変換することを「エンコード」、変換されたデータを元の形に戻すことを「デコード」と呼びます。

JSON ⇆ Swift間で効率よくデコード・エンコードを行うには、以下の2通りの方法があります。

①JSON ⇆ Foundation型の変換

JSONとSwiftの標準的なデータ型(Dictionary型など)とでデコード・エンコードを行うには、JSONSerializationクラスを利用します。

JSONSerializationクラスを用いたデコード・エンコードの方法については、以下の記事で解説しています。

②JSON ⇆ カスタム構造体の変換

JSONとSwiftのカスタム構造体とでデコード・エンコードを行うには、JSONDecoder・JSONEncoderクラスを利用します。

今回の記事では、こちらの方法をご紹介します。

基本的な変換ケース4選

4つの典型的なケースを題材に、JSONDecoder・JSONEncoderクラスを利用したデコード・エンコードの具体的な方法を説明します。

ケース① JSON → カスタム構造体へのデコード方法

まずは、JSONをSwiftのカスタム構造体にデコードする基本的な方法を説明します。

STEP
変換元のJSONと変換先のカスタム構造体を用意する

まずは、変換元のJSONデータと変換先のSwiftカスタム構造体を定義します。以下は果物を表現するJSONデータとカスタム構造体の例です。

//変換元のJSON
let fruitJSON = """
{
    "name": "Banana",
    "price": 140,
    "country": "Philippines"
}
""".data(using: .utf8)!
//変換先のカスタム構造体
struct Fruit: {
    let name: String
    let price: Int
    let country: String
}

JSON・カスタム構造体ともに、果物の名前を表すnameプロパティ、価格を表すpriceプロパティ、原産国を表すcountryプロパティを持っており、プロパティの名称と数は両データで完全に一致しています。

STEP
カスタム構造体をCodableプロトコルに準拠させる

JSONDecoderクラスによるデコードを行うには、カスタム構造体をCodableプロトコルに準拠させます。

SETP1で定義したFruit構造体を以下のように変更します。

//Codableプロトコルに準拠させる
struct Fruit: Codable{
    let name: String
    let price: Int
    let country: String
}

デコードを行う際にはDecodableプロトコル、エンコードを行う際にはEncodableプロトコルに準拠する必要があります。

これらのプロトコルに同時に準拠できるのがCodableプロトコルです。Codableプロトコルは、型エイリアスとプロトコルコンポジションを用いて以下のように定義されています。

typealias Codable = Decodable & Encodable

通常はCodableプロトコルに準拠させておくと、デコード・エンコードの両方に対応でき便利です。

STEP
JSONDecoderのdecodeメソッドでデコードを行う

JSONDecoderクラスのインスタンスを作成し、decode(_:from:)メソッドを用いてデコードを行います。

let decoder = JSONDecoder()

let fruitSwift = try decoder.decode(Fruit.self, from: fruitJSON)

print(fruitSwift) //Fruit(name: "Banana", price: 140, country: "Philippines")

decode(_:from:)メソッドの第一引数には変換先のカスタム構造体の型(Fruit.self)を渡し、第二引数fromには変換元のJSONデータを渡します。

GOAL
JSON → カスタム構造体へのデコード完了!
<参考>ケース①のコード全文
import Foundation

let fruitJSON = """
{
    "name": "Banana",
    "price": 140,
    "country": "Philippines"
}
""".data(using: .utf8)!

struct Fruit: Codable{
    let name: String
    let price: Int
    let country: String
}

let decoder = JSONDecoder()

let fruitSwift = try decoder.decode(Fruit.self, from: fruitJSON)

print(fruitSwift)

ケース②カスタム構造体 → JSONへのエンコード方法

ケース①と反対に、Swiftのカスタム構造体をJSONにエンコードする方法を説明します。

STEP
カスタム構造体のインスタンスを作成する

まずは、変換元となるカスタム構造体のインスタンスを作成します。以下のようにFruit構造体のインスタンスを作成し、定数fruitSwiftに格納します。

struct Fruit: Codable{
    let name: String
    let price: Int
    let country: String
}

let fruitSwift = Fruit(name: "Banana", price: 140, country: "Philippines")

デコード時と同様に、Fruit構造体はCodableプロトコルに準拠させます。

STEP
JSONEncoderクラスのencodeメソッドで変換を行う

JSONEncoderクラスのインスタンスを作成し、encodeメソッドを用いてエンコードを行います。

let encoder = JSONEncoder()

let fruitJSON = try encoder.encode(fruitSwift)

print(String(data: fruitJSON, encoding: .utf8)!) //{"name":"Banana","price":140,"country":"Philippines"}

encodeメソッドの引数には、変換元となるカスタム構造体のインスタンスを渡します。

GOAL
カスタム構造体 → JSONへのエンコード完了!
<参考>ケース②のコード全文
struct Fruit: Codable{
    let name: String
    let price: Int
    let country: String
}

let fruitSwift = Fruit(name: "Banana", price: 140, country: "Philippines")

let encoder = JSONEncoder()

let fruitJSON = try encoder.encode(fruitSwift)

print(String(data: fruitJSON, encoding: .utf8)!)

ケース③JSON・Swift間でプロパティ名が異なる場合の変換方法

JSONのプロパティ名とSwiftカスタム構造体のプロパティ名が異なる場合には、少し注意が必要です。

STEP
プロパティ名の不一致箇所を確認する

JSONのプロパティ名とSwiftカスタム構造体のプロパティ名との相違箇所を確認します。

let fruitJSON = """
{
    "name_of_fruit": "Banana", //JSON側は"name_of_fruit"と表記
    "price": 140,
    "country": "Philippines"
}
""".data(using: .utf8)!
struct Fruit: Codable{
    let name: String //Swift側は"name"と表記
    let price: Int
    let country: String
}

上記の例では、果物の名前を表すプロパティ名が、JSON側ではname_of_fruit、Swiftカスタム構造体側ではnameとなっています。

このようにJSON・Swift間でプロパティ名が異なる場合、ケース①②の方法でデコード・エンコードを行おうとするとエラーが発生します。

STEP
カスタム構造体にコーディングキーを実装する

プロパティ名が異なるケースではCodingKeyを作成します。CodingKeyとはプロパティの「一覧表」のようなもので、異なる名称のプロパティどうしを紐づける役割も持っています。

以下は、Swiftカスタム構造体におけるnameプロパティとJSONにおけるname_of_fruitを紐づけるCodingKeyの実装例となります。

struct Fruit: Codable{
    let name: String
    let price: Int
    let country: String
    
    enum CodingKeys: String, CodingKey{
        case name = "name_of_fruit"
        case price
        case country
    }
}

コーディングキーは列挙型として定義し、エンコード・デコードを行う構造体の内部にネストさせます。各ケースにはSwiftカスタム構造体のプロパティを持たせます。また、列挙型にはString型のローバリューを指定しCodingKeyプロトコルに準拠させます。

以下のように、ローバリューにJSON側のプロパティ名を記入することによって、名称の異なるプロパティどうしの紐付けを行うことができます。

case name = "name_of_fruit"

これでnamename_of_fruitは同じプロパティであると見なされます。あとはJSONDecoderを用いて通常通りにデコード・エンコードを行うことができます!

GOAL
プロパティ名が異なる場合の変換完了!
<参考>ケース③のコード全文
let fruitJSON = """
{
    "name_of_fruit": "Banana",
    "price": 140,
    "country": "Philippines"
}
""".data(using: .utf8)!

struct Fruit: Codable{
    let name: String
    let price: Int
    let country: String
    
    enum CodingKeys: String, CodingKey{
        case name = "name_of_fruit"
        case price
        case country
    }
}

let decoder = JSONDecoder()
let fruitSwift = try decoder.decode(Fruit.self, from: fruitJSON)
print(fruitSwift)

ケース④ 特定のプロパティを対象外にして変換する方法

不要なプロパティがある場合には、そのプロパティを変換対象外とすることができます。

STEP
不要なプロパティをコーディングキーから除外する

不要なプロパティがある場合には、そのプロパティをCodingKeys列挙型のケースから省くことで変換対象外にできます。

let fruitJSON = """
{
    "name": "Banana",
    "price": 140,
    "country": "Philippines",
    "color": "yellow" //"color"は不要なプロパティ
}
""".data(using: .utf8)!
struct Fruit: Codable{
    let name: String
    let price: Int
    let country: String
    
    enum CodingKeys: CodingKey{
        case name
        case price
        case country
    } //colorはケースから省く
}

上記の例において、JSONのプロパティのうちcolorプロパティが不要だとします。その場合CodingKeys列挙型にはname,price, countryの3つのケースのみを記述しcolorはケースに含めないようにします。

STEP
エンコード・デコードを行う

変換の「一覧表」にあたるコーディングキーに記載のないプロパティについては、Swiftが自動で省いてくれます。あとはケース①②と同じ手順でエンコード・デコードを行うことができます。

GOAL
特定のプロパティを対象外にして変換完了!
<参考>ケース④のコード全文
let fruitJSON = """
{
    "name": "Banana",
    "price": 140,
    "country": "Philippines",
    "color": "yellow"
}
""".data(using: .utf8)!

struct Fruit: Codable{
    let name: String
    let price: Int
    let country: String
    
    enum CodingKeys: String, CodingKey{
        case name
        case price
        case country
    }
}

let decoder = JSONDecoder()
let fruitSwift = try decoder.decode(Fruit.self, from: fruitJSON)
print(fruitSwift)

少し複雑な変換ケース2選

「JSONとSwiftカスタム構造体の間で、プロパティの階層構造が異なる場合」には、少し変換に手間がかかります。

具体例を見てみましょう。以下は、あるユーザの情報を表すJSONデータです。

{
  "id": 1,
  "fullName": "Yamada Taro",
  "birthYear": 1980,
  "address": {
    "prefecture": "Kanagawa",
    "city": "Yokohama"
  }
}

JSONが保持するプロパティのうち、ユーザの住所を示すaddressプロパティは、内部に県を表すprefectureプロパティと、市を表すcityプロパティを保持しています。

このJSONと以下のSwiftカスタム構造体との間で、デコード・エンコードを行う場合を考えます。

struct User{
    let id: Int
    let fullName: String
    let birthYear: Int
    let prefecture: String
    let city: String
}

カスタム構造体側の定義では、prefectureプロパティとcityプロパティは他のプロパティと同じ階層で保持され、JSON側で見たようなaddressプロパティの内部にネストする形式をとっていません。

このようにプロパティの階層の深さが異なるデータどうしでデコード・エンコードを行う場合には、ひと工夫が必要となります。

ケース⑤階層構造を持つJSONをデコードする場合

まずはデコードの方法を見てみましょう。

STEP
User構造体をDecodableプロトコルに準拠させる

デコードを行うUser構造体をDecodableプロトコルに準拠させます。今回のケースではデコードのみを行う想定なので、CodableプロトコルではなくDecodableプロトコルに準拠させています。

struct User: Decodable{
    let id: Int
    let fullName: String
    let birthYear: Int
    let prefecture: String
    let city: String
}

Decodableプロトコルへの準拠条件はイニシャライザinit(from:)を実装することです。通常はSwiftが暗黙的に準備しているため、特に意識することなく準拠できます。

ただし今回のように階層構造をもつJSONをデコードするケースでは、デフォルトで用意されたイニシャライザをそのまま利用できないため、手動でカスタマイズする必要があります。

STEP
イニシャライザのデフォルト実装を確認する

カスタマイズを行う前に、Swiftが暗黙的に実装したイニシャライザの中身を確認してみます。

カスタム構造体のプロパティの下で「ini」と入力すると、候補としてinit(from decoder: any Decoder) {...}が表示されます。

これを選択すると入力補完が行われ、暗黙的に用意されたイニシャライザとコーディングキーの中身を確認することができます。

struct User: Decodable{
    let id: Int
    let fullName: String
    let birthYear: Int
    let prefecture: String
    let city: String
    
    //暗黙的に用意されたコーディングキー
    enum CodingKeys: CodingKey {
        case id
        case fullName
        case birthYear
        case prefecture
        case city
    }
    
    //暗黙的に用意されたイニシャライザ
    init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(Int.self, forKey: .id)
        self.fullName = try container.decode(String.self, forKey: .fullName)
        self.birthYear = try container.decode(Int.self, forKey: .birthYear)
        self.prefecture = try container.decode(String.self, forKey: .prefecture)
        self.city = try container.decode(String.self, forKey: .city)
    }

}

コーディングキーには特に目新しい点はありませんので、イニシャライザの中身を見ていきます。

まず1行目です。

let container = try decoder.container(keyedBy: CodingKeys.self)

ここではDecoderプロトコルのcontainer(keyedBy:)メソッドを利用してコンテナを作成し、それを定数containerに格納しています。

コンテナとは「すべてのキーと値のペアを保持している容れ物」のようなもので、container(keyedBy:)メソッドの引数に渡した列挙型のキーとJSONの各プロパティとが紐付けされた状態で、内部に格納されます。

次に、2行目です。

self.id = try container.decode(Int.self, forKey: .id)

ここでは、コンテナのdecode(_:forKey:)メソッドを用いて、コンテナに格納された値を取り出す処理が行われています。メソッドの第一引数に型を、第二引数にキーを渡すことで、キーに紐づく値を指定した型で取り出すことができます。

そして、取り出した値をself.idに代入しています。この作業を2~6行目で行い、すべてのプロパティが初期化する仕組みになっています。

STEP
イニシャライザとコーディングキーをカスタマイズして、コンテナをネストさせる

STEP2まででデフォルトの実装の中身を理解することができたので、JSONの階層構造を取り扱えるようにカスタマイズを行なっていきます。

階層構造を表現するためには、コンテナをネストさせます。まずは以下のように、コーディングキーを2つに分解します。

enum CodingKeys: CodingKey {
    case id
    case fullName
    case birthYear
    case address
}

enum AddressCodingKeys: CodingKey{
    case prefecture
    case city
}

CodingKeys列挙型にaddressケースを追加しました。またAddressCodingKeysという新たなコーディングキーを作成し、その中でprefecturecityケースを作成しました。

コーディングキーに変更を加えたことによってイニシャライザでエラーが発生するので、修正を行います。

init(from decoder: any Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.id = try container.decode(Int.self, forKey: .id)
    self.fullName = try container.decode(String.self, forKey: .fullName)
    self.birthYear = try container.decode(Int.self, forKey: .birthYear)
    
    let addressContainer = try container.nestedContainer(keyedBy: AddressCodingKeys.self, forKey: .address)
    self.prefecture = try addressContainer.decode(String.self, forKey: .prefecture)
    self.city = try addressContainer.decode(String.self, forKey: .city)
}

ここでは定数addressContainerを新たに作成し、コンテナのnestedContainer(keyedBy:forKey:)メソッドを用いて、既存のコンテナにネストさせる形で初期化を行なっています。こうすることでJSONの階層構造を表現することができます。

id, fullName, birthYearプロパティは通常のコンテナ(container)から、prefecture, cityプロパティはネストされたコンテナ(addressContainer)から、それぞれdecodeメソッドで取り出して、各プロパティに格納します。

これで各プロパティの初期化が正常に完了し、エラーが解消されます。カスタマイズは完了です!
あとはJSONDecoderを用いて通常通りデコードを行うことができます。

GOAL
階層構造を持つJSONのデコード完了!
<参考>ケース⑤のコード全文
let jsonData = """
{
  "id": 1,
  "fullName": "Yamada Taro",
  "birthYear": 1980,
  "address": {
    "prefecture": "Kanagawa",
    "city": "Yokohama"
  }
}
""".data(using: .utf8)!

struct User: Decodable{
    let id: Int
    let fullName: String
    let birthYear: Int
    let prefecture: String
    let city: String
    
    enum CodingKeys: CodingKey {
        case id
        case fullName
        case birthYear
        case address
    }
    
    enum AddressCodingKeys: CodingKey{
        case prefecture
        case city
    }
    
    init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(Int.self, forKey: .id)
        self.fullName = try container.decode(String.self, forKey: .fullName)
        self.birthYear = try container.decode(Int.self, forKey: .birthYear)
        
        let addressContainer = try container.nestedContainer(keyedBy: AddressCodingKeys.self, forKey: .address)
        self.prefecture = try addressContainer.decode(String.self, forKey: .prefecture)
        self.city = try addressContainer.decode(String.self, forKey: .city)
    }
}

let data = try JSONDecoder().decode(User.self, from: jsonData)
print(data)

ケース⑥ 階層構造を持つJSONへエンコードする場合

次にエンコードの方法を見ていきます。

struct User{
    let id: Int
    let fullName: String
    let birthYear: Int
    let prefecture: String
    let city: String
}

let user = User(id: 1, fullName: "Yamada Taro", birthYear: 1980, prefecture: "Kanagawa", city: "Yokohama")

上記のSwiftカスタム構造体を、

{
  "id": 1,
  "fullName": "Yamada Taro",
  "birthYear": 1980,
  "address": {
    "prefecture": "Kanagawa",
    "city": "Yokohama"
  }
}

このようなJSONに変換するケースを考えます。ケース⑤と同様にプロパティの階層の深さが異なるため、カスタマイズが必要になります。

STEP
User構造体をEncodableプロトコルに準拠させる

エンコードを行うUser構造体をEncodableプロトコルに準拠させます。今回はエンコードのみを行うので、CodableプロトコルではなくEncodableプロトコルに準拠させます。

struct User: Encodable{
    let id: Int
    let fullName: String
    let birthYear: Int
    let prefecture: String
    let city: String
}

Encodableプロトコルへの準拠条件はメソッドencode(to encoder:)メソッドを実装することです。通常はSwiftが暗黙的に準備しているため、特に意識することなく準拠できます。

ただし階層構造をもつJSONにエンコードするケースでは、デフォルトで用意されたメソッドをそのまま利用できないため、手動でカスタマイズする必要があります。

STEP
メソッドのデフォルト実装を確認する

カスタマイズを行う前に、Swiftが暗黙的に実装したメソッドの中身を確認してみます。

カスタム構造体のプロパティの下で「en」と入力すると、候補としてencode(to encoder: any Encoder){...}が表示されます。

これを選択すると入力補完が行われ、暗黙的に用意されたメソッドとコーディングキーの中身を確認することができます。

struct User: Encodable{
    let id: Int
    let fullName: String
    let birthYear: Int
    let prefecture: String
    let city: String
    
    //暗黙的に用意されたコーディングキー
    enum CodingKeys: CodingKey {
        case id
        case fullName
        case birthYear
        case prefecture
        case city
    }
    
    //暗黙的に用意されたメソッド
    func encode(to encoder: any Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.id, forKey: .id)
        try container.encode(self.fullName, forKey: .fullName)
        try container.encode(self.birthYear, forKey: .birthYear)
        try container.encode(self.prefecture, forKey: .prefecture)
        try container.encode(self.city, forKey: .city)
    }
}

メソッド内ではEncoderプロトコルのcontainer(keyedBy:)メソッドを利用してコンテナを作成し、変数containerに格納しています。

var container = encoder.container(keyedBy: CodingKeys.self)

このコンテナは中身が空の状態で生成されるので、2行目以降でコンテナのencode(_:forKey:)メソッドを利用して中身を追加します。containerが定数でなく変数となっているのはそのためです。

try container.encode(self.id, forKey: .id)

encode(_:forKey:)メソッドの第一引数に格納する値を、第二引数にキーを渡すことで、値とキーを紐づけた状態でコンテナに格納できます。

この作業を2~6行目で行い、すべてのプロパティをコンテナに格納しています。

STEP
メソッドとコーディングキーをカスタマイズして、コンテナをネストさせる

STEP2まででデフォルトの実装の中身を理解することができたので、JSONの階層構造を取り扱えるようにカスタマイズを行なっていきます。

階層構造を表現するためには、コンテナをネストさせます。方法はデコード時と同じなので割愛します。

enum CodingKeys: CodingKey {
    case id
    case fullName
    case birthYear
    case address
}

enum AddressCodingKeys: CodingKey{
    case prefecture
    case city
}

func encode(to encoder: any Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(self.id, forKey: .id)
    try container.encode(self.fullName, forKey: .fullName)
    try container.encode(self.birthYear, forKey: .birthYear)
    
    var addressContainer = container.nestedContainer(keyedBy: AddressCodingKeys.self, forKey: .address)
    try addressContainer.encode(self.prefecture, forKey: .prefecture)
    try addressContainer.encode(self.city, forKey: .city)
}

これでカスタマイズは完了です!
あとはJSONEncoderを用いて通常通りにエンコードを行うことができます。

GOAL
階層構造を持つJSONへのエンコード完了!
<参考>ケース⑥のコード全文
struct User: Encodable{
    let id: Int
    let fullName: String
    let birthYear: Int
    let prefecture: String
    let city: String
    
    enum CodingKeys: CodingKey {
        case id
        case fullName
        case birthYear
        case address
    }
    
    enum AddressCodingKeys: CodingKey{
        case prefecture
        case city
    }
    
    func encode(to encoder: any Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.id, forKey: .id)
        try container.encode(self.fullName, forKey: .fullName)
        try container.encode(self.birthYear, forKey: .birthYear)
        
        var addressContainer = container.nestedContainer(keyedBy: AddressCodingKeys.self, forKey: .address)
        try addressContainer.encode(self.prefecture, forKey: .prefecture)
        try addressContainer.encode(self.city, forKey: .city)
    }
}

let user = User(id: 1, fullName: "Yamada Taro", birthYear: 1980, prefecture: "Kanagawa", city: "Yokohama")
let jsonUser = try JSONEncoder().encode(user)
print(String(data: jsonUser, encoding: .utf8)!)

デコード・エンコード両方行うことが想定される場合には、Codableプロトコルに準拠してinit(from:)encode(to encoder:)を両方カスタマイズ実装すればOKです!

おわりに

今回は、JSON⇆Swiftカスタム構造体間のデコード・エンコードについて紹介しました。
本文中で紹介しきれなかった細かい仕様に関しては、以下の参考文献をご覧ください!

以上、参考になれば嬉しいです!
ここまでお読みくださり、ありがとうございました????

※本記事は、著者が学習した内容をまとめたものとなります。内容の精査につきましては、執筆時の技術力で可能な限りの注意を払っていますが、万が一誤りがございましたらフォームからご一報いただけると幸いです????????

参考文献

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次