อาร์เรย์ถอดรหัส 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

ทางเลือกหนึ่งคือการใช้ประเภท wrapper ที่พยายามถอดรหัสค่าที่กำหนด การจัดเก็บ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
จะเกิดอะไรขึ้นถ้าวัตถุฐานไม่ใช่อาร์เรย์ แต่มีอยู่? ชอบ {"products": [{"name": "banana" ... }, ... ]}
ludvigeriksson

2
@ludvigeriksson คุณแค่ต้องการถอดรหัสภายในโครงสร้างนั้นเช่นgist.github.com/hamishknight/c6d270f7298e4db9e787aecb5b98bcae
Hamish

1
Swift's Codable เป็นเรื่องง่ายจนถึงตอนนี้ .. มันทำให้ง่ายกว่านี้ไม่ได้เหรอ?
จอนนี่

@ Hamish ฉันไม่เห็นข้อผิดพลาดในการจัดการสำหรับบรรทัดนี้ จะเกิดอะไรขึ้นหากมีข้อผิดพลาดเกิดขึ้นที่นี่var container = try decoder.unkeyedContainer()
bibscy

@bibscy มันอยู่ในเนื้อความinit(from:) throwsดังนั้น Swift จะเผยแพร่ข้อผิดพลาดกลับไปยังผู้โทรโดยอัตโนมัติ (ในกรณีนี้คือตัวถอดรหัสซึ่งจะแพร่กระจายกลับไปที่การJSONDecoder.decode(_:from:)โทร)
Hamish

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ประเภท Wrapper (มากกว่า 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

ปัญหาคือเมื่อทำซ้ำบนคอนเทนเนอร์ container.currentIndex จะไม่เพิ่มขึ้นดังนั้นคุณสามารถลองถอดรหัสอีกครั้งด้วยประเภทอื่น

เนื่องจาก currentIndex เป็นแบบอ่านอย่างเดียววิธีแก้ปัญหาคือการเพิ่มตัวเองด้วยตัวคุณเองที่สามารถถอดรหัสหุ่นจำลองได้สำเร็จ ฉันใช้วิธีแก้ปัญหา @ แฮมิชและเขียน wrapper ด้วย 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 และมีโครงสร้างเพิ่มเติมที่ง่ายที่สุด (ไม่มี generics!) ดังนั้นฉันคิดว่ามันควรจะเป็นที่ยอมรับ
Alper

2
นี่ควรเป็นคำตอบที่ได้รับการยอมรับ คำตอบใด ๆ ที่ทำให้โมเดลข้อมูลของคุณเสียหายถือเป็นการแลกเปลี่ยน imo ที่ไม่สามารถยอมรับได้
Joe Susnick

21

มีสองทางเลือก:

  1. ประกาศให้สมาชิกทั้งหมดของโครงสร้างเป็นทางเลือกซึ่งคีย์อาจหายไป

    struct GroceryProduct: Codable {
        var name: String
        var points : Int?
        var description: String?
    }
  2. เขียน initializer แบบกำหนดเองเพื่อกำหนดค่าเริ่มต้นใน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 แต่บางครั้งจะเป็น String ใน JSON ดังนั้นฉันจึงลองทำตามที่คุณกล่าวไว้ข้างต้นด้วยdeviceName = try values.decodeIfPresent(Int.self, forKey: .deviceName) ?? 00000ดังนั้นหากล้มเหลวมันจะใส่ 0000 แต่ก็ยังล้มเหลว
Martheli

ในกรณีนี้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 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)

ฉันได้รวมโซลูชันนี้ไว้ในส่วนขยายgithub.com/IdleHandsApps/SafeDecoder
Fraser

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

คำตอบของ @ Hamish เยี่ยมมาก อย่างไรก็ตามคุณสามารถลด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.isAtEndEmptyDecodable


0

ความพยายามที่ง่ายกว่ามาก: ทำไมคุณไม่ประกาศคะแนนเป็นทางเลือกหรือทำให้อาร์เรย์มีองค์ประกอบที่เป็นทางเลือก

let products = [GroceryProduct?]
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.