[iOS] Decodable でJSONをデコードする時に要素の型を変換する その3 KeyedDecodingContainer のエクステンションと Property Wrapper で一部の要素にのみ独自のデコード処理を適用しつつ狙った型に変換する

はじめに(承前)

本ブログの 以前のポスト で以下のJSONをデコードしようとしました。

{
    "食品名":"肉類",
    "食材": "鶏肉, 豚肉, 牛肉",
    "栄養": ["タンパク質", "ビタミンA", "ビタミンB"]
}

食材 (String) と 栄養 (String の配列) を、どちらも文字列の配列でデコードしたいという話でした。
そのためのコードとして以下を書きました。

struct Food: Decodable {
    let 食品名: String
    let 食材: [Foodstuff]  // 独自の型に変更
    let 栄養: [String]
}

struct Foodstuff: Decodable {
    let value: String
}

extension KeyedDecodingContainer {
    func decode(_: [Foodstuff].Type, forKey key: Key) throws -> [Foodstuff] {
        let valueString = try decodeIfPresent(String.self, forKey: key)
        let valueArray = valueString?.components(separatedBy: ",")
        return valueArray?.map { Foodstuff(value: $0) } ?? []  // 独自の型に変更
    }
}

[Foodstuff] という型に変えてしまえば、KeyedDecodingContainer のエクステンションで
食材 要素のみに独自のデコード処理を適応できます。
で、 Foodstuff.first?.value とかで文字列を取得できます。

いやー[String] で扱いたいですよね!

KeyedDecodingContainer のエクステンションではなくinit(from decoder: Decoder) throws {
自前でデコード処理を書けば[String] でデコードできます。
が、それはそれで手間です。

ということで、「一部の要素にのみ独自のデコード処理を適用しつつ、狙った型に変換する手法」
を書いていきます。

タイトルが長い!

前提条件

Xcode: 12.4
Swift: 5.3

まずは最終的なコード

struct Food: Decodable {
    let 食品名: String
    @StringComponents var 食材: [String]
    let 栄養: [String]
}

extension KeyedDecodingContainer {
    func decode(_: StringComponents.Type, forKey key: Key) throws -> StringComponents {
        if let value = try decodeIfPresent(StringComponents.self, forKey: key) {
            return value
        } else {
            return StringComponents(wrappedValue: [])
        }
    }
}

@propertyWrapper
struct StringComponents: Decodable {
    var wrappedValue: [String] = []

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let valueString = try container.decode(String.self)
        wrappedValue = valueString.components(separatedBy: ",")
    }
    
    init(wrappedValue: [String]) {
        self.wrappedValue = wrappedValue
    }
}

これで狙い通り、食材[String] として扱えます。

説明

食材 に、定義した StringComponentsPropertyWrapper を適応します。
(PropertyWrapper の基本的な挙動については、
以前のポスト[iOS] Property Wrapper の概要について にてまとめています)

KeyedDecodingContainer のエクステンションに
func decode(_: StringComponents.Type, forKey key: Key) throws -> StringComponents {
を定義します。

そうすると食材 をデコードする時のみ、その処理を通ります。

その処理の中で if let value = try decodeIfPresent(StringComponents.self, forKey: key) { でデコードを試みます。

実際のデコード処理は StringComponentsinit(from decoder: Decoder) throws { にて行います。
ここで String[String] に変換しています。

あとは StringComponents を返却して、食材 のデコードは完了です。

結果をデバッガを見ると以下のようになります。

デコード結果

左のペインでは _食材 というプロパティ名で表示されますが、
.食材 でアクセスすると [String] で取得できています。


また、KeyedDecodingContainer のエクステンションを変更して、
以下のように返り値などを [String] にしても同様のデコード結果になります。

extension KeyedDecodingContainer {
    func decode(_: StringComponents.Type, forKey key: Key) throws -> [String] {
        if let valueString = try decodeIfPresent(String.self, forKey: key) {
            return valueString.components(separatedBy: ",")
        } else {
            return []
        }
    }
}

プロパティラッパと欲しい型、どちらを返却すると筋がいいかまでは理解が及んでいません🙏

説明は以上となります!

最後に

デコード周りは一区切りとします。
また何か知見が出てきたら書くかもしれません。

以上です。
お疲れ様でした!

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google フォト

Google アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中

%d人のブロガーが「いいね」をつけました。