単一の要素のデコードが失敗した場合、Swift JSONDecodeのデコード配列が失敗する


116

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の場合はデフォルト値を割り当てることができます。
Viniアプリ

1
なぜpointsオプションとして宣言できないのですか?
NRitH 2018年

回答:


115

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

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

1
基本オブジェクトが配列ではないが配列が含まれている場合はどうなりますか?{"products"のように:[{"name": "banana" ...}、...]}
ludvigeriksson

2
@ludvigeriksson次に、その構造内でデコードを実行するだけです。たとえば、gist.github.com
Hamish

1
SwiftのCodableは、これまでは簡単でした。これを少し単純にすることはできませんか?
ジョニー

@ハミッシュ私はこの行のエラー処理を見ていません。ここでエラーがスローされるとどうなりますかvar container = try decoder.unkeyedContainer()
びびる

@bibscyこれはの本体内にあるinit(from:) throwsため、Swiftは自動的にエラーを呼び出し元に返します(この場合、デコーダーが呼び出しにエラーを返しJSONDecoder.decode(_:from:)ます)。
Hamish

33

私は新しい型を作成します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スローされるエラーとそのインデックスを追跡するのに役立つ場合があるため)します。

スウィフト5

Swift 5の場合、egの使用を検討してくださいResult enum

struct Throwable<T: Decodable>: Decodable {
    let result: Result<T, Error>

    init(from decoder: Decoder) throws {
        result = Result(catching: { try T(from: decoder) })
    }
}

デコードされた値をアンラップするget()には、resultプロパティのメソッドを使用します:

let products = throwables.compactMap { try? $0.result.get() }

私はカスタムを書くことを心配する必要がないので、この答えが好きですinit
Mihai Fratu

これは私が探していた解決策です。それはとてもクリーンで簡単です。これありがとう!
naturaln0va

24

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

currentIndexは読み取り専用であるため、解決策は自分で増分してダミーを正常にデコードすることです。私は@Hamishソリューションを使用して、カスタムのinitでラッパーを作成しました。

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

ここに投稿されたソリューションは、コメントの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)

1
代わりの一変形形態では、if/else私が使用do/catch内側while私はエラーをログに記録できるようにループ
フレーザー

2
この回答はSwiftバグトラッカーについて言及しており、最も単純な追加の構造体(ジェネリックなし!)を持っているので、それは受け入れられるものであると思います。
Alper、2018

2
これは受け入れられる答えになるはずです。データモデルを破壊する答えは受け入れられないトレードオフです。
Joe Susnick

21

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) ?? ""
        }
    }

5
代わりtry?decodeそれが使用することをお勧めしますtrydecodeIfPresent第二の選択肢に。デフォルト値を設定する必要があるのは、キーがない場合のみで、キーが存在するがタイプが間違っている場合など、デコードに失敗した場合は必要ありません。
user28434 2017

@vadian、タイプが一致しない場合にデフォルト値を割り当てるためのカスタム初期化子に関連する他のSOの質問を知っていますか?私はIntであるキーを持っていますが、JSONの文字列になることがあります。そのため、上で述べたように試してみたdeviceName = try values.decodeIfPresent(Int.self, forKey: .deviceName) ?? 00000ので、失敗した場合は0000が挿入されますが、それでも失敗します。
Martheli

この場合、キーが存在するためdecodeIfPresent、誤りAPIです。別のdo - catchブロックを使用してください。デコードString、エラーが発生した場合はデコードInt
vadian

13

プロパティラッパーを使用してSwift 5.1によって可能になったソリューション:

@propertyWrapper
struct IgnoreFailure<Value: Decodable>: Decodable {
    var wrappedValue: [Value] = []

    private struct _None: Decodable {}

    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()
        while !container.isAtEnd {
            if let decoded = try? container.decode(Value.self) {
                wrappedValue.append(decoded)
            }
            else {
                // item is silently ignored.
                try? container.decode(_None.self)
            }
        }
    }
}

そして使用法:

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

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

struct ProductResponse: Decodable {
    @IgnoreFailure
    var products: [GroceryProduct]
}


let response = try! JSONDecoder().decode(ProductResponse.self, from: json)
print(response.products) // Only contains banana.

注:プロパティラッパーは、応答を構造体でラップできる場合(つまり、最上位の配列ではない場合)にのみ機能します。その場合でも、手動でラップすることができます(読みやすくするためにエイリアスを使用)。

typealias ArrayIgnoringFailure<Value: Decodable> = IgnoreFailure<Value>

let response = try! JSONDecoder().decode(ArrayIgnoringFailure<GroceryProduct>.self, from: json)
print(response.wrappedValue) // Only contains banana.

7

Iveは、@ 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)


3

残念ながら、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

最近同じような問題がありましたが、少し異なりました。

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

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

そして、このエッジケースを処理する正しい方法は、以下のように[String]文字列配列をオプションの文字列の配列として宣言することです[String?]

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

2

すべての配列に対してこの動作が必要になるように、@ Hamishを改善しました。

private struct OptionalContainer<Base: Codable>: Codable {
    let base: Base?
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        base = try? container.decode(Base.self)
    }
}

private struct OptionalArray<Base: Codable>: Codable {
    let result: [Base]
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let tmp = try container.decode([OptionalContainer<Base>].self)
        result = tmp.compactMap { $0.base }
    }
}

extension Array where Element: Codable {
    init(from decoder: Decoder) throws {
        let optionalArray = try OptionalArray<Element>(from: decoder)
        self = optionalArray.result
    }
}

1

@ハミッシュの答えは素晴らしいです。ただし、次のように削減できます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

代わりに、次のようにすることもできます。

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

そしてそれを取得している間:

'let groceryList = try JSONDecoder().decode(Array<GroceryProduct>.self, from: responseData)'

0

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

より簡単な試み:ポイントをオプションとして宣言したり、配列にオプションの要素を含めたりしないでください。

let products = [GroceryProduct?]
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.