स्विफ्ट JSONDecode डिकोडिंग सरणियों में विफल रहता है यदि एकल तत्व डिकोडिंग विफल हो जाता है


116

स्विफ्ट 4 और कोडेबल प्रोटोकॉल का उपयोग करते समय मुझे निम्नलिखित समस्या मिली - ऐसा लगता है कि JSONDecoderकिसी सरणी में तत्वों को छोड़ने की अनुमति देने का कोई तरीका नहीं है । उदाहरण के लिए, मेरे पास निम्नलिखित JSON है:

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

और ए कोडेबल संरचना:

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

जब इस जशन को डिकोड करते हैं

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

रिजल्ट productsखाली है। इस बात की उम्मीद की जा रही है कि इस तथ्य के कारण कि JSON में दूसरी वस्तु नहीं है"points" कुंजी , , जबकि संरचना pointsमें वैकल्पिक नहीं है GroceryProduct

प्रश्न यह है कि मैं JSONDecoderअमान्य ऑब्जेक्ट को "स्किप" कैसे कर सकता हूं ?


हम अमान्य वस्तुओं को छोड़ नहीं सकते, लेकिन यदि यह शून्य है तो आप डिफ़ॉल्ट मान निर्दिष्ट कर सकते हैं।
विनी ऐप

1
pointsकेवल वैकल्पिक घोषित क्यों नहीं किया जा सकता है?
एनआरआईटीएच

जवाबों:


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
क्या होगा यदि आधार ऑब्जेक्ट एक सरणी नहीं है, लेकिन इसमें एक है? जैसे {"उत्पादों": [{"नाम": "केला" ...}, ...]}
ludvigeriksson

2
@ludvigeriksson आप केवल उस संरचना के भीतर डिकोडिंग करना चाहते हैं, उदाहरण के लिए: gist.github.com/hamishknight/c6d270f7298e4db9e787aecb5b98bcae
Hamish

1
स्विफ्ट का कोडेबल आसान था, अब तक .. क्या इसे सरल नहीं बनाया जा सकता है?
जॉनी

@ हमीश को इस लाइन के लिए कोई त्रुटि नहीं दिखाई दे रही है। यदि कोई त्रुटि यहाँ फेंकी जाती है तो क्या होता हैvar container = try decoder.unkeyedContainer()
bibscy

@bibscy यह शरीर के भीतर है init(from:) throws, इसलिए स्विफ्ट स्वचालित रूप से कॉलर को वापस त्रुटि का प्रचार करेगा (इस मामले में डिकोडर, जो इसे 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आवरण प्रकार (एक से अधिक) का उपयोग करने का विकल्प चुनूंगाStruct ) क्योंकि यह उन त्रुटियों का ट्रैक रखने के लिए उपयोगी हो सकता है जो उनके सूचकांकों के साथ-साथ फेंके गए हैं।

स्विफ्ट 5

स्विफ्ट 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 में वृद्धि नहीं होती है ताकि आप एक अलग प्रकार के साथ फिर से डिकोड करने का प्रयास कर सकें।

क्योंकि करंटइंडेक्स केवल पढ़ा जाता है, एक समाधान यह है कि इसे स्वयं बढ़ाया जाए और सफलतापूर्वक डमी को डिकोड किया जाए। मैंने @ हैमिश समाधान लिया, और एक कस्टम इनिट के साथ एक आवरण लिखा।

यह समस्या एक वर्तमान स्विफ्ट बग है: 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
इस उत्तर में स्विफ्ट बग ट्रैकर का उल्लेख है और इसमें सबसे सरल अतिरिक्त संरचना (कोई जेनरिक नहीं है!) इसलिए मुझे लगता है कि इसे स्वीकार किया जाना चाहिए।
Alper

2
यह स्वीकृत उत्तर होना चाहिए। आपके डेटा मॉडल को दूषित करने वाला कोई भी जवाब अस्वीकार्य ट्रेडऑफ imo है।
जो सुसनिक

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दूसरा विकल्प में। हमें केवल डिफ़ॉल्ट मान सेट करने की आवश्यकता है यदि कोई कुंजी नहीं है, किसी भी डिकोडिंग विफलता के मामले में नहीं, जैसे कि कुंजी मौजूद है, लेकिन प्रकार गलत है।
15:28 बजे user28434

हे @ वियनडियन क्या आप जानते हैं कि किसी प्रकार के एसओ प्रश्न शामिल होते हैं जो कस्टम इनिशियलाइज़र से जुड़े होते हैं जो कि प्रकार से मेल नहीं खाने के मामले में डिफ़ॉल्ट मान निर्दिष्ट करते हैं? मेरे पास एक कुंजी है जो एक Int है लेकिन कभी-कभी JSON में एक स्ट्रिंग होगी इसलिए मैंने ऐसा करने की कोशिश की जो आपने ऊपर कहा है deviceName = try values.decodeIfPresent(Int.self, forKey: .deviceName) ?? 00000अगर यह विफल रहता है तो यह सिर्फ 0000 डाल देगा लेकिन यह अभी भी विफल रहता है।
मार्तली

इस मामले decodeIfPresentमें गलत है APIक्योंकि कुंजी मौजूद है। दूसरे do - catchब्लॉक का उपयोग करें । डिकोड String, यदि कोई त्रुटि होती है, तो डिकोड करेंInt
vadian

13

संपत्ति के आवरण का उपयोग करके स्विफ्ट 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 विस्तार का उपयोग करने के लिए कुछ संशोधनों के साथ, @ सोफी-स्विकज़ समाधान डाल दिया

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

दुर्भाग्य से स्विफ्ट 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

मैंने मामले में @ हमीश के लिए सुधार किया, कि आप सभी सरणियों के लिए यह व्यवहार चाहते हैं:

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 Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.