この記事でわかること
- 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のカスタム構造体にデコードする基本的な方法を説明します。
まずは、変換元の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
プロパティを持っており、プロパティの名称と数は両データで完全に一致しています。
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
プロトコルに準拠させておくと、デコード・エンコードの両方に対応でき便利です。
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データを渡します。
<参考>ケース①のコード全文
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にエンコードする方法を説明します。
まずは、変換元となるカスタム構造体のインスタンスを作成します。以下のように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
プロトコルに準拠させます。
JSONEncoder
クラスのインスタンスを作成し、encode
メソッドを用いてエンコードを行います。
let encoder = JSONEncoder()
let fruitJSON = try encoder.encode(fruitSwift)
print(String(data: fruitJSON, encoding: .utf8)!) //{"name":"Banana","price":140,"country":"Philippines"}
encode
メソッドの引数には、変換元となるカスタム構造体のインスタンスを渡します。
<参考>ケース②のコード全文
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カスタム構造体のプロパティ名が異なる場合には、少し注意が必要です。
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間でプロパティ名が異なる場合、ケース①②の方法でデコード・エンコードを行おうとするとエラーが発生します。
プロパティ名が異なるケースでは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"
これでname
とname_of_fruit
は同じプロパティであると見なされます。あとはJSONDecoder
を用いて通常通りにデコード・エンコードを行うことができます!
<参考>ケース③のコード全文
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)
ケース④ 特定のプロパティを対象外にして変換する方法
不要なプロパティがある場合には、そのプロパティを変換対象外とすることができます。
不要なプロパティがある場合には、そのプロパティを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
はケースに含めないようにします。
変換の「一覧表」にあたるコーディングキーに記載のないプロパティについては、Swiftが自動で省いてくれます。あとはケース①②と同じ手順でエンコード・デコードを行うことができます。
<参考>ケース④のコード全文
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をデコードする場合
まずはデコードの方法を見てみましょう。
デコードを行う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をデコードするケースでは、デフォルトで用意されたイニシャライザをそのまま利用できないため、手動でカスタマイズする必要があります。
カスタマイズを行う前に、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行目で行い、すべてのプロパティが初期化する仕組みになっています。
STEP2まででデフォルトの実装の中身を理解することができたので、JSONの階層構造を取り扱えるようにカスタマイズを行なっていきます。
階層構造を表現するためには、コンテナをネストさせます。まずは以下のように、コーディングキーを2つに分解します。
enum CodingKeys: CodingKey {
case id
case fullName
case birthYear
case address
}
enum AddressCodingKeys: CodingKey{
case prefecture
case city
}
CodingKeys
列挙型にaddress
ケースを追加しました。またAddressCodingKeys
という新たなコーディングキーを作成し、その中でprefecture
と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)
}
ここでは定数addressContainer
を新たに作成し、コンテナのnestedContainer(keyedBy:forKey:)
メソッドを用いて、既存のコンテナにネストさせる形で初期化を行なっています。こうすることでJSONの階層構造を表現することができます。
id
, fullName
, birthYear
プロパティは通常のコンテナ(container
)から、prefecture
, city
プロパティはネストされたコンテナ(addressContainer
)から、それぞれdecode
メソッドで取り出して、各プロパティに格納します。
これで各プロパティの初期化が正常に完了し、エラーが解消されます。カスタマイズは完了です!
あとはJSONDecoder
を用いて通常通りデコードを行うことができます。
<参考>ケース⑤のコード全文
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に変換するケースを考えます。ケース⑤と同様にプロパティの階層の深さが異なるため、カスタマイズが必要になります。
エンコードを行う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にエンコードするケースでは、デフォルトで用意されたメソッドをそのまま利用できないため、手動でカスタマイズする必要があります。
カスタマイズを行う前に、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行目で行い、すべてのプロパティをコンテナに格納しています。
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
を用いて通常通りにエンコードを行うことができます。
<参考>ケース⑥のコード全文
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カスタム構造体間のデコード・エンコードについて紹介しました。
本文中で紹介しきれなかった細かい仕様に関しては、以下の参考文献をご覧ください!
以上、参考になれば嬉しいです!
ここまでお読みくださり、ありがとうございました????
※本記事は、著者が学習した内容をまとめたものとなります。内容の精査につきましては、執筆時の技術力で可能な限りの注意を払っていますが、万が一誤りがございましたらフォームからご一報いただけると幸いです????????
参考文献
- Apple Inc. “Codable”. AppleDeveloper, (参照 2024-05-22)
- Apple Inc. “Decodable”. AppleDeveloper, (参照 2024-05-22)
- Apple Inc. “Encodable”. AppleDeveloper, (参照 2024-05-22)
- Apple Inc. “Decoder”. AppleDeveloper, (参照 2024-05-22)
- Apple Inc. “Encoder”. AppleDeveloper, (参照 2024-05-22)
- Apple Inc. “JSONDecoder”. AppleDeveloper, (参照 2024-05-22)
- Apple Inc. “JSONEncoder”. AppleDeveloper, (参照 2024-05-22)
- Apple Inc. “CodingKey”. AppleDeveloper, (参照 2024-05-22)
- Apple Inc. “KeyedDecodingContainer”. AppleDeveloper, (参照 2024-05-22)
- Apple Inc. “Using JSON with Custom Types”. AppleDeveloper, (参照 2024-05-22)
->Access Nested DataとMerge Data at Different Depthsの章についてはかなり複雑なケース(JSONとデコード先の構造体のデータ構造が異なる場合に中間型を挟んでデコードを行う方法・JSONとカスタム構造体の階層構造が著しく異なる場合のデコード・エンコード方法)なので、今回の記事からは省いている。実際のアプリ開発のシーンでこれらの知識を利用する場面が出てきたら追記する予定。 - 石川洋資, 西山勇世. [増補改訂第3版]Swift実践入門 直感的な文法と安全性を兼ね備えた言語. 技術評論社, 2021. p192~193, p204
- Paul Hudson. Hacking with iOS(SwiftUI Edition). Hacking with Swift. 2024. p343~344, p368~p370
->@Observableを利用する際のCodingKeyのカスタマイズ方法について記述されている。@Observableに関する記事を執筆したのちに、追記する予定。 - Swiftful Thinking. “Codable, Decodable, and Encodable in Swift | Continued Learning #21”. Youtube. 2021-04, (参照 2024-05-22)
コメント