Подтвердить что ты не робот

Массивы декодирования Swift JSONDecode терпят неудачу, если сбой одного элемента

При использовании протоколов 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 "пропускать" недопустимый объект?

4b9b3361

Ответ 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.")
//    )
// ]

Затем мы используем .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.")
//    )
// ]

Ответ 2

Проблема заключается в том, что при выполнении итерации по контейнеру container.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)

Ответ 3

Я хотел бы создать новый тип 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), потому что может быть полезно отслеживать ошибки, которые выдают, а также их индексы.

Swift 5

Для Swift 5 Попробуйте использовать Result enum, например,

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

    init(from decoder: Decoder) throws {
        do {
            let decoded = try T.init(from: decoder)
            result = .success(decoded)
        } catch let error {
            result = .failure(error)
        }
    }
}

Чтобы развернуть декодированное значение, используйте метод get() свойства result:

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

Ответ 4

Есть два варианта:

  • Объявить всех членов структуры как необязательных, ключи которых могут отсутствовать

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

Ответ 5

Я добавил решение @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)

Ответ 6

К сожалению, у 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)")
}

Ответ 7

У меня недавно была похожая проблема, но немного другая.

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

В этом случае, если один из элементов в friendnamesArray равен nil, весь объект равен нулю при декодировании.

И правильный способ справиться с этим крайним случаем - объявить строковый массив [String] как массив необязательных строк [String?], как показано ниже,

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

Ответ 8

@Отличный ответ. Однако вы можете уменьшить 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)
    }
}

Ответ 9

Я улучшил @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
    }
}

Ответ 10

Решение стало возможным благодаря 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 для лучшей читаемости):

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

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

Ответ 11

Я придумаю этот 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.

Ответ 12

Гораздо более простая попытка: Почему бы вам не объявить точки как необязательные или сделать массив содержащим необязательные элементы

let products = [GroceryProduct?]

Ответ 13

Я столкнулся с той же проблемой и не нашел ни одного ответа, удовлетворяющего.

У меня была следующая структура:

public struct OfferResponse {
public private(set) var offers: [Offer]

public init(data: Data) throws {
    let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: [Any]]
    guard let offersDataArray = json?["Offers"] else {
        throw NSError(domain: "unexpected JSON structure for \(type(of: self))", code: 36, userInfo: nil)
    }
    guard let firstOfferData = offersDataArray.first else {
        throw NSError(domain: "emptyArray in JSON structure for \(type(of: self))", code: 36, userInfo: nil)
    }
    let decoder = JSONDecoder()
    offers = try decoder.decode([Offer].self, from: JSONSerialization.data(withJSONObject: firstOfferData, options: .prettyPrinted))
}

В какой-то момент сервер вернул плохое содержимое для элемента. Я решил это так:

    offers = []
    for offerData in offersDataArray {
        if let offer = try? decoder.decode(Offer.self, from: JSONSerialization.data(withJSONObject: offerData, options: .prettyPrinted)) {
            offers.append(offer)
        }