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


117

При использовании протоколов 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в GroceryProductstruct не является обязательным .

Вопрос в том, как я могу позволить JSONDecoder«пропустить» недопустимый объект?


Мы не можем пропустить недопустимые объекты, но вы можете назначить значения по умолчанию, если они равны нулю.
Vini App

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" ...}, ...]}
людвигерикссон

2
@ludvigeriksson Тогда вы просто хотите выполнить декодирование в этой структуре, например: gist.github.com/hamishknight/c6d270f7298e4db9e787aecb5b98bcae
Хэмиш

1
Codable от Swift до сих пор было легко ... неужели это не может быть проще?
Джонни

@Hamish Я не вижу обработки ошибок для этой строки. Что произойдет, если здесь будет var container = try decoder.unkeyedContainer()
выдана

@bibscy Это внутри тела init(from:) throws, поэтому Swift автоматически передаст ошибку обратно вызывающему (в данном случае декодеру, который передаст ее обратно JSONDecoder.decode(_:from:)вызову).
Хэмиш,

34

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

Swift 5

Для Swift 5 рассмотрите возможность использования egResult 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
Михай Фрату

Это решение, которое я искал. Это так чисто и просто. Спасибо тебе за это!
naturaln0va,

24

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

1
Один вариант, вместо того, чтобы if/elseиспользовать do/catchвнутри whileцикла, чтобы я мог регистрировать ошибку
Фрейзер

2
В этом ответе упоминается трекер ошибок Swift и есть простейшая дополнительная структура (без дженериков!), Поэтому я думаю, что она должна быть принятой.
Alper

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во втором варианте. Нам нужно установить значение по умолчанию только в том случае, если ключа нет, а не в случае сбоя декодирования, например, когда ключ существует, но тип неправильный.
user28434

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

В этом случае decodeIfPresentэто неправильно, APIпотому что ключ действительно существует. Используйте другой do - catchблок. Расшифровать String, если возникнет ошибка, расшифроватьInt
vadian

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 для лучшей читаемости):

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 с некоторыми изменениями в простое в использовании расширение

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)

Я завернул это решение в расширение github.com/IdleHandsApps/SafeDecoder
Фрейзер

3

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

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 для случая, когда вы хотите, чтобы такое поведение было для всех массивов:

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

2

Вместо этого вы также можете сделать так:

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

а затем при получении:

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

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

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.