Tablice dekodowania Swift JSONDecode kończą się niepowodzeniem, jeśli nie powiedzie się dekodowanie pojedynczego elementu

116

Podczas korzystania z protokołów Swift4 i Codable napotkałem następujący problem - wygląda na to, że nie ma sposobu, aby pozwolić JSONDecoderna pominięcie elementów w tablicy. Na przykład mam następujący kod JSON:

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

I Codable struct:

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

Podczas dekodowania tego pliku json

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

Wynik productsjest pusty. Czego można się spodziewać, ze względu na fakt, że drugi obiekt w JSON nie ma "points"klucza, a pointsnie jest opcjonalny w GroceryProductstrukturze.

Pytanie brzmi, jak mogę pozwolić JSONDecoderna „pominięcie” nieprawidłowego obiektu?

Khriapin Dmitriy
źródło
Nie możemy pominąć nieprawidłowych obiektów, ale możesz przypisać wartości domyślne, jeśli są zerowe.
Vini App
1
Dlaczego nie można pointspo prostu uznać, że jest opcjonalny?
NRitH

Odpowiedzi:

115

Jedną z opcji jest użycie typu opakowania, które próbuje zdekodować daną wartość; przechowywanie w nilprzypadku niepowodzenia:

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

Następnie możemy zdekodować ich tablicę, GroceryProductwypełniając Basesymbol zastępczy:

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

Następnie używamy .compactMap { $0.base }do odfiltrowywania nilelementów (tych, które spowodowały błąd podczas dekodowania).

Spowoduje to utworzenie pośredniej tablicy [FailableDecodable<GroceryProduct>], która nie powinna stanowić problemu; jednak jeśli chcesz tego uniknąć, zawsze możesz utworzyć inny typ opakowania, który dekoduje i rozpakowuje każdy element z kontenera bez klucza:

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

Następnie dekodowałbyś jako:

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.")
//    )
// ]
Hamish
źródło
1
Co się stanie, jeśli obiekt podstawowy nie jest tablicą, ale ją zawiera? Podobnie jak {"products": [{"name": "banana" ...}, ...]}
ludvigeriksson
2
@ludvigeriksson Po prostu chcesz wykonać dekodowanie w ramach tej struktury, na przykład: gist.github.com/hamishknight/c6d270f7298e4db9e787aecb5b98bcae
Hamish
1
Kodowanie Swifta było łatwe, aż do teraz… czy nie można tego uczynić prostszym?
Jonny
@Hamish Nie widzę żadnej obsługi błędów dla tej linii. Co się stanie, jeśli pojawi się tutaj błądvar container = try decoder.unkeyedContainer()
bibscy
@bibscy Jest w treści init(from:) throws, więc Swift automatycznie przekaże błąd z powrotem do wywołującego (w tym przypadku dekodera, który prześle go z powrotem do JSONDecoder.decode(_:from:)wywołania).
Hamish
34

Utworzyłbym nowy typ Throwable, który może opakować dowolny typ zgodny z 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)
        }
    }
}

Do dekodowania tablicy GroceryProduct(lub dowolnej innej Collection):

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

gdzie valuejest obliczoną własnością wprowadzoną w rozszerzeniu na Throwable:

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

Wybrałbym użycie enumtypu opakowania (powyżej a Struct), ponieważ może być przydatne śledzenie zgłaszanych błędów, a także ich indeksów.

Szybki 5

Dla Swift 5 Rozważ użycie npResult enum

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

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

Aby rozpakować zdekodowaną wartość, użyj get()metody na resultwłaściwości:

let products = throwables.compactMap { try? $0.result.get() }
cfergie
źródło
Podoba mi się ta odpowiedź, ponieważ nie muszę się martwić o pisanie niestandardowychinit
Mihai Fratu
To jest rozwiązanie, którego szukałem. To takie czyste i proste. Dziękuję Ci za to!
naturaln0va
24

Problem polega na tym, że podczas iteracji po kontenerze element container.currentIndex nie jest zwiększany, więc możesz spróbować ponownie zdekodować z innym typem.

Ponieważ currentIndex jest tylko do odczytu, rozwiązaniem jest samodzielne zwiększenie jego wartości, pomyślne dekodowanie atrapy. Wziąłem rozwiązanie @Hamish i napisałem opakowanie z niestandardowym initem.

Ten problem jest aktualnym błędem Swift: https://bugs.swift.org/browse/SR-5953

Opublikowane tutaj rozwiązanie to obejście w jednym z komentarzy. Podoba mi się ta opcja, ponieważ analizuję kilka modeli w ten sam sposób na kliencie sieciowym i chciałem, aby rozwiązanie było lokalne dla jednego z obiektów. Oznacza to, że nadal chcę, aby pozostałe zostały odrzucone.

Lepiej wyjaśniam na moim githubie 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)
Sophy Swicz
źródło
1
Jedna odmiana, zamiast if/elseużywam do/catchwewnątrz whilepętli, więc mogę zarejestrować błąd
Fraser
2
Ta odpowiedź wspomina o narzędziu do śledzenia błędów Swift i ma najprostszą dodatkową strukturę (bez generics!), Więc myślę, że powinna być akceptowana.
Alper,
2
To powinna być akceptowana odpowiedź. Każda odpowiedź, która psuje model danych, jest niedopuszczalnym kompromisem.
Joe Susnick
21

Istnieją dwie możliwości:

  1. Zadeklaruj wszystkich członków struktury jako opcjonalne, których kluczy może brakować

    struct GroceryProduct: Codable {
        var name: String
        var points : Int?
        var description: String?
    }
  2. Napisz niestandardowy inicjator, aby przypisać domyślne wartości w nilprzypadku.

    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) ?? ""
        }
    }
vadian
źródło
5
Zamiast try?ze decodelepiej jest użyć tryze decodeIfPresentw drugiej opcji. Musimy ustawić wartość domyślną tylko wtedy, gdy nie ma klucza, a nie w przypadku błędu dekodowania, na przykład gdy klucz istnieje, ale typ jest nieprawidłowy.
user28434
hej @vadian czy znasz jakieś inne pytania SO dotyczące niestandardowego inicjatora do przypisywania wartości domyślnych w przypadku niezgodności typu sprawy? Mam klucz, który jest Int, ale czasami będzie to String w JSON, więc próbowałem zrobić to, co powiedziałeś powyżej, deviceName = try values.decodeIfPresent(Int.self, forKey: .deviceName) ?? 00000więc jeśli się nie powiedzie, po prostu wstawi 0000, ale nadal się nie powiedzie.
Martheli
W tym przypadku decodeIfPresentjest źle, APIponieważ klucz istnieje. Użyj innego do - catchbloku. Dekodowanie String, jeśli wystąpi błąd, dekodowanieInt
vadian
13

Rozwiązanie, które umożliwił Swift 5.1, wykorzystując opakowanie właściwości:

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

A potem użycie:

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.

Uwaga: Elementy opakowania właściwości będą działać tylko wtedy, gdy odpowiedź może być opakowana w strukturę (tj. Nie jest tablicą najwyższego poziomu). W takim przypadku możesz nadal zawinąć go ręcznie (aliasem typu dla lepszej czytelności):

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

let response = try! JSONDecoder().decode(ArrayIgnoringFailure<GroceryProduct>.self, from: json)
print(response.wrappedValue) // Only contains banana.
rraphael
źródło
7

Umieściłem rozwiązanie @ sophy-swicz, z pewnymi modyfikacjami, w łatwym w użyciu rozszerzeniu

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

Po prostu nazwij to tak

init(from decoder: Decoder) throws {

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

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

W powyższym przykładzie:

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)
Fraser
źródło
Zapakowałem to rozwiązanie w rozszerzenie github.com/IdleHandsApps/SafeDecoder
Fraser
3

Niestety API Swift 4 nie ma dostępnego inicjatora dla init(from: Decoder).

Jedynym rozwiązaniem, które widzę, jest implementacja niestandardowego dekodowania, podając domyślną wartość dla pól opcjonalnych i możliwy filtr z potrzebnymi danymi:

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)")
}
dimpiax
źródło
2

Ostatnio miałem podobny problem, ale trochę inny.

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

W tym przypadku, jeśli jeden z elementów w friendnamesArrayjest zerowy, cały obiekt jest zerowy podczas dekodowania.

Prawidłowym sposobem obsługi tego skrajnego przypadku jest zadeklarowanie tablicy ciągów [String]jako tablicy opcjonalnych ciągów, [String?]jak poniżej,

struct Person: Codable {
    var name: String
    var age: Int
    var description: String?
    var friendnamesArray:[String?]?
}
cnu
źródło
2

Poprawiłem @ Hamish dla przypadku, w którym chcesz tego zachowania dla wszystkich tablic:

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
    }
}
Sören Schmaljohann
źródło
1

Odpowiedź @ Hamisha jest świetna. Możesz jednak zredukować FailableCodableArraydo:

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)
    }
}
Robert Crabtree
źródło
1

Zamiast tego możesz też zrobić w ten sposób:

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

a potem w trakcie pobierania:

'let groceryList = try JSONDecoder().decode(Array<GroceryProduct>.self, from: responseData)'
Kalpesh Thakare
źródło
0

Wymyśliłem to, KeyedDecodingContainer.safelyDecodeArrayco zapewnia prosty interfejs:

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

Potencjalnie nieskończona pętla while !container.isAtEndjest problemem i można ją rozwiązać za pomocą EmptyDecodable.

Haoxin Li
źródło
0

O wiele prostsza próba: dlaczego nie zadeklarujesz punktów jako opcjonalnych lub nie utworzysz tablicy zawierającej elementy opcjonalne

let products = [GroceryProduct?]
BobbelKL
źródło