Швидкий масив декодування JSONDecode виходить з ладу, якщо декодування одного елемента не вдається


116

Під час використання протоколів Swift4 та Codable у мене виникла наступна проблема - схоже, немає можливості дозволити JSONDecoderпропускати елементи в масиві. Наприклад, у мене є такий JSON:

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

І структура Codable :

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 не має "points"ключа, в той час як pointsвін не є необов’язковим у GroceryProductструктурі.

Питання в тому, як я можу дозволити JSONDecoder"пропустити" недійсний об'єкт?


Ми не можемо пропустити недійсні об'єкти, але ви можете призначити значення за замовчуванням, якщо вони нульові.
Додаток Vini

1
Чому pointsпросто не можна оголосити факультативом?
NRitH

Відповіді:


115

Один варіант - використовувати тип обгортки, який намагається декодувати задане значення; зберігання, 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.")
//    )
// ]

Потім ми використовуємо .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
Що робити, якщо базовий об'єкт не є масивом, але він містить його? Like {"products": [{"name": "banana" ...}, ...]}
ludvigeriksson

2
@ludvigeriksson Ви просто хочете виконати розшифровку в цій структурі, наприклад: gist.github.com/hamishknight/c6d270f7298e4db9e787aecb5b98bcae
Hamish

1
Codable Swift був легким, до цих пір .. Хіба це не може бути спрощено?
Джоні

@Hamish Я не бачу жодних помилок для цього рядка. Що трапиться, якщо тут буде закинута помилкаvar container = try decoder.unkeyedContainer()
bibscy

@bibscy Це в тілі init(from:) throws, тому Swift автоматично поширює помилку назад на абонент (у цьому випадку декодер, який передасть його назад до JSONDecoder.decode(_:from:)виклику).
Гаміш

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тип обгортки (понад a Struct), оскільки це може бути корисно для відстеження помилок, які викидаються, а також їх індексів.

Швидкий 5

Для Swift 5 Подумайте про використання напр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

Проблема полягає в тому, що при ітерації над контейнером контейнер.currentIndex не збільшується, тому ви можете спробувати знову розшифрувати інший тип.

Оскільки currentIndex лише для читання, рішення - збільшити його, успішно розшифрувавши манекен. Я взяв рішення @Hamish і написав обгортку зі спеціальним init.

Ця проблема є поточною помилкою Swift: https://bugs.swift.org/browse/SR-5953

Рішення, розміщене тут, є вирішенням одного з коментарів. Мені подобається цей варіант, тому що я аналогічно розбираю купу моделей на мережевому клієнті, і я хотів, щоб рішення було локальним для одного з об’єктів. Тобто я все ще хочу, щоб інші були відкинуті.

Я пояснюю краще в своєму 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/elseI використовую do/catchвнутрішню whileпетлю, щоб я міг зафіксувати помилку
Fraser

2
Ця відповідь згадує трекер помилок Swift і має найпростішу додаткову структуру (без дженериків!), Тому я думаю, що вона повинна бути прийнятою.
Альпер

2
Це має бути прийнятою відповіддю. Будь-яка відповідь, яка пошкоджує вашу модель даних, є неприйнятним компромісом.
Джо Суснік

21

Є два варіанти:

  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його краще використовувати tryз decodeIfPresentв другому варіанті. Нам потрібно встановити значення за замовчуванням лише в тому випадку, якщо немає ключа, не в разі помилки декодування, як, наприклад, ключ існує, але тип неправильний.
користувач28434

hey @vadian Чи знаєте ви будь-які інші питання, пов'язані з тим, що стосуються спеціального ініціалізатора для призначення значень за замовчуванням у випадку, якщо тип не відповідає? У мене є ключ, який є Int, але іноді буде струною в JSON, тому я спробував зробити те, що ви сказали вище, deviceName = try values.decodeIfPresent(Int.self, forKey: .deviceName) ?? 00000так що якщо він не вдасться, він просто поставить 0000, але він все ще не вдається.
Мартелі

У цьому випадку decodeIfPresentце неправильно, APIоскільки ключ існує. Використовуйте інший do - catchблок. Розшифруйте String, якщо виникає помилка, розшифруйтеInt
вадиан

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)

Ive завершив це рішення у розширенні github.com/IdleHandsApps/SafeDecoder
Fraser

3

На жаль, API Swift 4 не має доступного ініціалізатора для 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]?
}

У цьому випадку, якщо один з елементів у friendnamesArrayнулі, то весь об'єкт є нульовим під час декодування.

І правильним способом обробки цього крайового випадку є оголошення рядкового масиву [String]як масиву необов'язкових рядків, [String?]як показано нижче,

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

2

Я вдосконалив на справі @ Hamish's, що ви хочете, щоб ця поведінка була для всіх масивів:

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?]
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.