Swift4とCodableのプロトコルを使用している間、私は次のような問題を抱えていました - JSONDecoder
name__が配列内の要素をスキップすることを許可する方法がないようです。たとえば、私は次の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
name__は空です。 points
name__はGroceryProduct
name__構造体ではオプションではありませんが、JSONの2番目のオブジェクトには"points"
キーがないという事実のために、これは予想されることです。
JSONDecoder
name__に無効なオブジェクトを「スキップ」させるにはどうすればよいですか。
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)
}
}
それから、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.")
// )
// ]
それから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.")
// )
// ]
2つの選択肢があります。
キーがなくなる可能性がある構造体のすべてのメンバをオプションとして宣言します。
struct GroceryProduct: Codable {
var name: String
var points : Int?
var description: String?
}
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) ?? ""
}
}
問題は、コンテナを反復処理するときに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)
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
ラッパー型の使用を選択します。
@ 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)
残念ながら、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)")
}
@ 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)
}
}
私は最近同様の問題を抱えていましたが、わずかに異なります。
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?]?
}
シンプルなインターフェースを提供するこの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
を使用することによって対処されます。
もっと簡単な試み:ポイントをオプションとして宣言したり、配列にオプションの要素を含めたりしないのはなぜですか
let products = [GroceryProduct?]