Tek öğeli kod çözme başarısız olursa Swift JSOND kod çözme dizileri başarısız olur


116

Swift4 ve Kodlanabilir protokolleri kullanırken şu sorunu yaşadım - JSONDecoderbir dizideki öğeleri atlamaya izin vermenin bir yolu yok gibi görünüyor . Örneğin, aşağıdaki JSON'a sahibim:

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

Ve Kodlanabilir bir yapı:

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

Bu json kodunu çözerken

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

Sonuç productsboş. JSON'daki ikinci nesnenin "points"anahtara sahip olmaması pointsve GroceryProductyapı içinde isteğe bağlı olmaması nedeniyle beklenen bir durumdur .

Soru, JSONDecodergeçersiz nesnenin "atlanmasına" nasıl izin verebilirim ?


Geçersiz nesneleri atlayamayız, ancak sıfır ise varsayılan değerler atayabilirsiniz.
Vini Uygulaması

1
Neden pointsisteğe bağlı ilan edilemiyor ?
NRitH

Yanıtlar:


115

Bir seçenek, belirli bir değerin kodunu çözmeye çalışan bir sarmalayıcı türü kullanmaktır; nilbaşarısız olursa depolama :

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

Ardından GroceryProduct, Baseyer tutucuyu doldurmanızla bunların bir dizisini çözebiliriz :

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

Daha sonra .compactMap { $0.base }filtrelemek için kullanıyoruznil öğeleri (kod çözme sırasında hata verenleri).

Bu, [FailableDecodable<GroceryProduct>]sorun olmaması gereken bir ara dizi oluşturacaktır ; ancak bundan kaçınmak isterseniz, her bir öğenin şifresini çözen ve her bir öğeyi anahtarlanmamış bir kaptan açan başka bir sarmalayıcı türü oluşturabilirsiniz:

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

Daha sonra şu şekilde deşifre edersiniz:

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
Ya temel nesne bir dizi değilse, ancak bir tane içeriyorsa? {"
Products

2
@ludvigeriksson Kod çözme işlemini bu yapı içinde gerçekleştirmek istiyorsunuz, örneğin: gist.github.com/hamishknight/c6d270f7298e4db9e787aecb5b98bcae
Hamish

1
Swift'in Kodlanabilirliği kolaydı, şimdiye kadar .. bu daha basit hale getirilemez mi?
Jonny

@Hamish Bu satırda herhangi bir hata işleme göremiyorum. Burada bir hata atılırsa ne olurvar container = try decoder.unkeyedContainer()
bibscy

@bibscy Bu, öğesinin içinde olduğundan init(from:) throws, Swift otomatik olarak hatayı arayana geri gönderir (bu durumda, kod çözücü, onu JSONDecoder.decode(_:from:)aramaya geri yayacaktır ).
Hamish

34

ThrowableAşağıdakilere uyan her türü sarabilen yeni bir tür oluşturardım 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)
        }
    }
}

Bir dizinin GroceryProduct(veya başka herhangi birinin Collection) kodunu çözmek için :

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

valuebir uzantıda sunulan hesaplanmış özellik nerede Throwable:

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

Bir enumsarmalayıcı türü kullanmayı tercih ederdim (birStruct ) çünkü atılan hataları ve bunların indekslerini takip etmek faydalı olabilir.

Swift 5

Swift 5 için Örn.Result enum

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

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

Kodu çözülen değeri çözmek get()için resultözellik üzerindeki yöntemi kullanın :

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

Bu cevabı beğendim çünkü herhangi bir özel yazma konusunda endişelenmeme gerek yokinit
Mihai Fratu

Aradığım çözüm bu. Çok temiz ve anlaşılır. Bunun için teşekkür ederim!
naturaln0va

24

Sorun, bir kapsayıcı üzerinde yineleme yaparken, container.currentIndex değerinin artırılmamasıdır, bu nedenle farklı bir türle yeniden kodu çözmeyi deneyebilirsiniz.

CurrentIndex salt okunur olduğundan, bir çözüm, bir kukla kod çözme işlemini kendi başınıza arttırmaktır. @Hamish çözümünü aldım ve özel init içeren bir sarmalayıcı yazdım.

Bu problem güncel bir Swift hatasıdır: https://bugs.swift.org/browse/SR-5953

Burada yayınlanan çözüm, yorumlardan birinde geçici bir çözümdür. Bu seçeneği seviyorum çünkü bir grup modeli bir ağ istemcisinde aynı şekilde ayrıştırıyorum ve çözümün nesnelerden biri için yerel olmasını istedim. Yani, hala diğerlerinin atılmasını istiyorum.

Github'ımda daha iyi açıklıyorum 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
Bir varyasyon, yerine ait if/elsebir kullanma do/catchiçine whilehatası log böylece döngü
Fraser

2
Bu cevap Swift hata takipçisinden bahsediyor ve en basit ek yapıya sahip (jenerik yok!), Bu yüzden kabul edilenin olması gerektiğini düşünüyorum.
Alper

2
Kabul edilen cevap bu olmalıdır. Veri modelinizi bozan herhangi bir yanıt, kabul edilemez bir ödünleşmedir.
Joe Susnick

21

İki seçenek vardır:

  1. Yapının tüm üyelerini, anahtarları eksik olabilen isteğe bağlı olarak bildirin

    struct GroceryProduct: Codable {
        var name: String
        var points : Int?
        var description: String?
    }
  2. Vakaya varsayılan değerler atamak için özel bir başlatıcı yazın 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
Yerine try?birlikte decodekullanmak daha iyidir tryile decodeIfPresentikinci seçenekte. Varsayılan değeri yalnızca anahtar yoksa, herhangi bir kod çözme başarısızlığı durumunda değil, anahtar var olduğunda, ancak tür yanlışsa ayarlamamız gerekir.
user28434

hey @vadian, türün eşleşmemesi durumunda varsayılan değerler atamak için özel başlatıcı içeren başka bir SO sorusu biliyor musunuz? Bir Int olan bir anahtarım var ama bazen JSON'da bir String olacak, bu yüzden yukarıda söylediklerinizi yapmaya çalıştım, deviceName = try values.decodeIfPresent(Int.self, forKey: .deviceName) ?? 00000bu yüzden başarısız olursa sadece 0000 koyacak ama yine de başarısız oluyor.
Martheli

Bu durumda decodeIfPresentyanlıştır APIçünkü anahtar mevcuttur. Başka bir do - catchblok kullanın . Kod çözme String, bir hata oluşursa, kod çözmeInt
vadian

13

Özellik sarmalayıcısı kullanılarak Swift 5.1 ile mümkün olan bir çözüm:

@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)
            }
        }
    }
}

Ve sonra kullanım:

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.

Not: Özellik sarmalayıcı şeyler yalnızca yanıt bir yapıya sarılabilirse çalışır (yani: üst düzey bir dizi değil). Bu durumda, yine de manuel olarak sarabilirsiniz (daha iyi okunabilirlik için bir tür takma adıyla):

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

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

7

@ Sophy-swicz çözümünü bazı değişikliklerle birlikte kullanımı kolay bir uzantıya koydum

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

Sadece böyle söyle

init(from decoder: Decoder) throws {

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

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

Yukarıdaki örnek için:

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

Maalesef Swift 4 API, init(from: Decoder) .

Gördüğüm tek çözüm, isteğe bağlı alanlar için varsayılan değer ve gerekli verilerle olası filtre veren özel kod çözme uygulamak:

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

Son zamanlarda benzer bir sorun yaşadım, ancak biraz farklı.

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

Bu durumda, içindeki elemanlardan friendnamesArraybiri sıfır ise, kod çözme sırasında tüm nesne sıfırdır.

Ve bu uç durumu ele almanın doğru yolu, dize dizisini aşağıdaki gibi [String]isteğe bağlı dizeler dizisi olarak bildirmektir.[String?]

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

2

Tüm diziler için bu davranışı istediğinizi @ Hamish'in durumu için geliştirdim:

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

@ Hamish'in cevabı harika. Ancak, şunları azaltabilirsiniz 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

Bunun yerine şunu da yapabilirsiniz:

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

ve sonra onu alırken:

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

0

KeyedDecodingContainer.safelyDecodeArrayBasit bir arayüz sağlayan bunu buldum :

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
}
}

Potansiyel olarak sonsuz döngü while !container.isAtEndbir sorundur ve kullanılarak ele alınır EmptyDecodable.


0

Çok daha basit bir girişim: Neden noktaları isteğe bağlı olarak bildirmiyorsunuz veya diziyi isteğe bağlı öğeler içermesini sağlamıyorsunuz?

let products = [GroceryProduct?]
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.