단일 요소 디코딩이 실패하면 Swift 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가 없지만 GroceryProductstruct 에서는 선택 사항이 아니기 때문에 예상 됩니다.

질문은 JSONDecoder잘못된 개체를 "건너 뛰기" 하는 방법입니다 .


유효하지 않은 객체를 건너 뛸 수는 없지만 nil이면 기본값을 할당 할 수 있습니다.
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
기본 개체가 배열이 아니지만 배열이 포함되어 있으면 어떻게됩니까? 좋아요 { "제품": [{ "name": "banana"...}, ...]}
ludvigeriksson

2
@ludvigeriksson 그런 다음 해당 구조 내에서 디코딩을 수행하기를 원합니다. 예 : gist.github.com/hamishknight/c6d270f7298e4db9e787aecb5b98bcae
Hamish

1
Swift의 Codable은 쉬웠습니다. 지금까지 .. 더 간단하게 만들 수 없나요?
Jonny

@Hamish이 줄에 대한 오류 처리가 보이지 않습니다. 여기에 오류가 발생하면 어떻게 var container = try decoder.unkeyedContainer()
되나요?

@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래퍼 유형 (이상 Struct) 을 사용하는 것을 선택합니다 .

스위프트 5

Swift 5의 경우 eg 사용을 고려하십시오.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
미하이 Fratu을

이것이 제가 찾던 솔루션입니다. 너무 깨끗하고 간단합니다. 감사합니다!
naturaln0va

24

문제는 컨테이너를 반복 할 때 container.currentIndex가 증가하지 않으므로 다른 유형으로 다시 디코딩을 시도 할 수 있다는 것입니다.

currentIndex는 읽기 전용이므로 해결 방법은 더미를 성공적으로 디코딩하여 직접 증가시키는 것입니다. @Hamish 솔루션을 사용하고 사용자 정의 초기화로 래퍼를 작성했습니다.

이 문제는 현재 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/catchwhile
Fraser

2
이 답변은 Swift 버그 추적기를 언급하고 가장 간단한 추가 구조체 (제네릭 없음!)를 가지고 있으므로 허용되는 것으로 생각합니다.
Alper

2
이것은 받아 들여진 대답이어야합니다. 데이터 모델을 손상시키는 모든 대답은 용납 할 수없는 트레이드 오프 imo입니다.
Joe Susnick

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그것을 사용하는 것이 좋습니다 trydecodeIfPresent두 번째 옵션으로. 키가 존재하는 경우와 같은 디코딩 실패의 경우가 아니라 키가없는 경우에만 기본값을 설정해야하지만 유형이 잘못되었습니다.
user28434

안녕하세요 @vadian 케이스 유형이 일치하지 않는 경우 기본값을 할당하는 사용자 지정 이니셜 라이저와 관련된 다른 SO 질문을 알고 있습니까? 나는 Int 키가 있지만 때로는 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 사용).

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
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가 nil이면 디코딩하는 동안 전체 객체가 nil입니다.

이 엣지 케이스를 처리하는 올바른 방법 은 아래와 같이 [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
    }
}

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

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?]
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.