web-dev-qa-db-ja.com

単一要素のデコードが失敗すると、Swift JSONDecodeデコード配列が失敗する

Swift4とCodableのプロトコルを使用している間、私は次のような問題を抱えていました - JSONDecodername__が配列内の要素をスキップすることを許可する方法がないようです。たとえば、私は次の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)

結果のproductsname__は空です。 pointsname__はGroceryProductname__構造体ではオプションではありませんが、JSONの2番目のオブジェクトには"points"キーがないという事実のために、これは予想されることです。

JSONDecodername__に無効なオブジェクトを「スキップ」させるにはどうすればよいですか。

68

1つの選択肢は、与えられた値をデコードしようと試みるラッパー型を使うことです。失敗した場合は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)
    }
}

それから、GroceryProductBaseのプレースホルダーに入力して、これらの配列をデコードすることができます。

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.")
//    )
// ]

それからnil要素(デコード時にエラーを投げかけた要素)を除外するために.compactMap { $0.base }を使います。

これは[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.")
//    )
// ]
80
Hamish

2つの選択肢があります。

  1. キーがなくなる可能性がある構造体のすべてのメンバをオプションとして宣言します。

    struct GroceryProduct: Codable {
        var name: String
        var points : Int?
        var description: String?
    }
    
  2. nilの場合にデフォルト値を割り当てるカスタム初期化子を作成します。

    struct GroceryProduct: Codable {
        var name: String
        var points : Int
        var description: String
    
        init(from decoder: Decoder) throws {
            let values = try decoder.container(keyedBy: CodingKeys.self)
            name = try values.decode(String.self, forKey: .name)
            points = try values.decodeIfPresent(Int.self, forKey: .points) ?? 0
            description = try values.decodeIfPresent(String.self, forKey: .description) ?? ""
        }
    }
    
16
vadian

問題は、コンテナを反復処理するときにcontainer.currentIndexがインクリメントされないため、別のタイプで再度デコードを試みることができることです。

CurrentIndexは読み取り専用なので、解決策は自分自身でインクリメントしてダミーを正しくデコードすることです。私は@Hamishソリューションを採用し、カスタムinitを使ったラッパーを書きました。

この問題は現在のSwiftのバグです。 https://bugs.Swift.org/browse/SR-595

ここに掲載されている解決策は、コメントの1つにある回避策です。私はネットワーククライアント上で同じ方法でたくさんのモデルを解析しているので、このオプションが好きです。そして、そのソリューションをオブジェクトの1つに対してローカルにすることを望みました。つまり、私はまだ他の人たちを捨てたいのです。

私はgithubでもっとよく説明しています https://github.com/phynet/Lossy-array-decode-Swift4

import Foundation

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

    private struct DummyCodable: Codable {}

    struct Groceries: Codable 
    {
        var groceries: [GroceryProduct]

        init(from decoder: Decoder) throws {
            var groceries = [GroceryProduct]()
            var container = try decoder.unkeyedContainer()
            while !container.isAtEnd {
                if let route = try? container.decode(GroceryProduct.self) {
                    groceries.append(route)
                } else {
                    _ = try? container.decode(DummyCodable.self) // <-- TRICK
                }
            }
            self.groceries = groceries
        }
    }

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

    let products = try JSONDecoder().decode(Groceries.self, from: json)

    print(products)
16
Sophy Swicz

Throwableに準拠する任意の型をラップできる、新しい型Decodableを作成します。

enum Throwable<T: Decodable>: Decodable {
    case success(T)
    case failure(Error)

    init(from decoder: Decoder) throws {
        do {
            let decoded = try T(from: decoder)
            self = .success(decoded)
        } catch let error {
            self = .failure(error)
        }
    }
}

GroceryProduct(またはその他のCollection)の配列をデコードする場合

let decoder = JSONDecoder()
let throwables = try decoder.decode([Throwable<GroceryProduct>].self, from: json)
let products = throwables.compactMap { $0.value }

valueは、Throwableの拡張で導入された計算プロパティです。

extension Throwable {
    var value: T? {
        switch self {
        case .failure(_):
            return nil
        case .success(let value):
            return value
        }
    }
}

スローされたエラーとそのインデックスを追跡することが役立つ場合があるので、(enumよりも)Structラッパー型の使用を選択します。

11
cfergie

@ sophy-swiczソリューションを、いくつかの変更を加えて、使いやすい拡張子に入れました。

fileprivate struct DummyCodable: Codable {}

extension UnkeyedDecodingContainer {

    public mutating func decodeArray<T>(_ type: T.Type) throws -> [T] where T : Decodable {

        var array = [T]()
        while !self.isAtEnd {
            do {
                let item = try self.decode(T.self)
                array.append(item)
            } catch let error {
                print("error: \(error)")

                // hack to increment currentIndex
                _ = try self.decode(DummyCodable.self)
            }
        }
        return array
    }
}
extension KeyedDecodingContainerProtocol {
    public func decodeArray<T>(_ type: T.Type, forKey key: Self.Key) throws -> [T] where T : Decodable {
        var unkeyedContainer = try self.nestedUnkeyedContainer(forKey: key)
        return try unkeyedContainer.decodeArray(type)
    }
}

このように呼ぶだけで

init(from decoder: Decoder) throws {

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

    self.items = try container.decodeArray(ItemType.self, forKey: . items)
}

上記の例では:

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

struct Groceries: Codable 
{
    var groceries: [GroceryProduct]

    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()
        groceries = try container.decodeArray(GroceryProduct.self)
    }
}

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

let products = try JSONDecoder().decode(Groceries.self, from: json)
print(products)
6
Fraser

残念ながら、Swift 4 APIはinit(from: Decoder)のための無効な初期化子を持っていません。

私が見ている唯一の解決策は、オプションのフィールドにデフォルト値を与え、必要なデータでフィルタをかけることができるカスタムデコードを実装することです。

struct GroceryProduct: Codable {
    let name: String
    let points: Int?
    let description: String

    private enum CodingKeys: String, CodingKey {
        case name, points, description
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        name = try container.decode(String.self, forKey: .name)
        points = try? container.decode(Int.self, forKey: .points)
        description = (try? container.decode(String.self, forKey: .description)) ?? "No description"
    }
}

// for test
let dict = [["name": "Banana", "points": 100], ["name": "Nut", "description": "Woof"]]
if let data = try? JSONSerialization.data(withJSONObject: dict, options: []) {
    let decoder = JSONDecoder()
    let result = try? decoder.decode([GroceryProduct].self, from: data)
    print("rawResult: \(result)")

    let clearedResult = result?.filter { $0.points != nil }
    print("clearedResult: \(clearedResult)")
}
2
dimpiax

@ Hamishの答えは素晴らしいです。ただし、FailableCodableArrayを次のように減らすことができます。

struct FailableCodableArray<Element : Codable> : Codable {

    var elements: [Element]

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let elements = try container.decode([FailableDecodable<Element>].self)
        self.elements = elements.compactMap { $0.wrapped }
    }

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

私は最近同様の問題を抱えていましたが、わずかに異なります。

struct Person: Codable {
    var name: String
    var age: Int
    var description: String?
    var friendnamesArray:[String]?
}

この場合、friendnamesArrayの要素の1つがnilであると、デコード中にオブジェクト全体がnilになります。

そしてこのEdgeのケースを扱う正しい方法は、以下のように文字列array[String]をオプションのstrings[String?]の配列として宣言することです。

struct Person: Codable {
    var name: String
    var age: Int
    var description: String?
    var friendnamesArray:[String?]?
}
0
cnu

シンプルなインターフェースを提供するこのKeyedDecodingContainer.safelyDecodeArrayを思い付きます。

extension KeyedDecodingContainer {

/// The sole purpose of this `EmptyDecodable` is allowing decoder to skip an element that cannot be decoded.
private struct EmptyDecodable: Decodable {}

/// Return successfully decoded elements even if some of the element fails to decode.
func safelyDecodeArray<T: Decodable>(of type: T.Type, forKey key: KeyedDecodingContainer.Key) -> [T] {
    guard var container = try? nestedUnkeyedContainer(forKey: key) else {
        return []
    }
    var elements = [T]()
    elements.reserveCapacity(container.count ?? 0)
    while !container.isAtEnd {
        /*
         Note:
         When decoding an element fails, the decoder does not move on the next element upon failure, so that we can retry the same element again
         by other means. However, this behavior potentially keeps `while !container.isAtEnd` looping forever, and Apple does not offer a `.skipFailable`
         decoder option yet. As a result, `catch` needs to manually skip the failed element by decoding it into an `EmptyDecodable` that always succeed.
         See the Swift ticket https://bugs.Swift.org/browse/SR-5953.
         */
        do {
            elements.append(try container.decode(T.self))
        } catch {
            if let decodingError = error as? DecodingError {
                Logger.error("\(#function): skipping one element: \(decodingError)")
            } else {
                Logger.error("\(#function): skipping one element: \(error)")
            }
            _ = try? container.decode(EmptyDecodable.self) // skip the current element by decoding it into an empty `Decodable`
        }
    }
    return elements
}
}

潜在的に無限ループのwhile !container.isAtEndは懸念であり、そしてそれはEmptyDecodableを使用することによって対処されます。

0
Haoxin Li

もっと簡単な試み:ポイントをオプションとして宣言したり、配列にオプションの要素を含めたりしないのはなぜですか

let products = [GroceryProduct?]
0
BobbelKL