1. ホーム
  2. arrays

[解決済み] SwiftのJSONDecodeで配列のデコードに失敗する(単一要素のデコードに失敗した場合

2022-06-30 07:54:31

質問

Swift4とCodableプロトコルを使用しているとき、次のような問題が発生しました。 JSONDecoder で配列の要素をスキップさせる方法はないようです。 例えば、私は次のようなJSONを持っています。

[
    {
        "name": "Banana",
        "points": 200,
        "description": "A banana grown in Ecuador."
    },
    {
        "name": "Orange"
    }
]

そして コーダブル 構造体もあります。

struct GroceryProduct: Codable {
    var name: String
    var points: Int
    var description: String?
}

このjsonをデコードすると

let decoder = JSONDecoder()
let products = try decoder.decode([GroceryProduct].self, from: json)

結果発表 products は空です。これは予想されることですが、JSONの2番目のオブジェクトには "points" キーがなく、一方 points はオプションではありません。 GroceryProduct 構造体のオプションではありません。

問題は、どのようにすれば JSONDecoder に無効なオブジェクトをスキップさせることができますか?

どのように解決するのですか?

一つの方法は、与えられた値をデコードしようとするラッパーの型を使用することです。 nil を格納します。

struct FailableDecodable<Base : Decodable> : Decodable {

    let base: Base?

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        self.base = try? container.decode(Base.self)
    }
}

そして、これらの配列をデコードするために、あなたの GroceryProduct を埋める Base のプレースホルダーを埋める。

import Foundation

let json = """
[
    {
        "name": "Banana",
        "points": 200,
        "description": "A banana grown in Ecuador."
    },
    {
        "name": "Orange"
    }
]
""".data(using: .utf8)!


struct GroceryProduct : Codable {
    var name: String
    var points: Int
    var description: String?
}

let products = try JSONDecoder()
    .decode([FailableDecodable<GroceryProduct>].self, from: json)
    .compactMap { $0.base } // .flatMap in Swift 4.0

print(products)

// [
//    GroceryProduct(
//      name: "Banana", points: 200,
//      description: Optional("A banana grown in Ecuador.")
//    )
// ]

次に .compactMap { $0.base } をフィルタリングするために nil 要素(デコード時にエラーを発生させたもの)を除外します。

これは、中間的な配列である [FailableDecodable<GroceryProduct>] しかし、もしこれを避けたいのであれば、キーが設定されていないコンテナから各要素をデコードしてラップ解除する別のラッパータイプを作成することができます。

struct FailableCodableArray<Element : Codable> : Codable {

    var elements: [Element]

    init(from decoder: Decoder) throws {

        var container = try decoder.unkeyedContainer()

        var elements = [Element]()
        if let count = container.count {
            elements.reserveCapacity(count)
        }

        while !container.isAtEnd {
            if let element = try container
                .decode(FailableDecodable<Element>.self).base {

                elements.append(element)
            }
        }

        self.elements = elements
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(elements)
    }
}

とデコードすることになります。

let products = try JSONDecoder()
    .decode(FailableCodableArray<GroceryProduct>.self, from: json)
    .elements

print(products)

// [
//    GroceryProduct(
//      name: "Banana", points: 200,
//      description: Optional("A banana grown in Ecuador.")
//    )
// ]