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

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



답변

한 가지 옵션은 주어진 값을 디코딩하는 래퍼 유형을 사용하는 것입니다. 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.")
//    )
// ]


답변

다음을 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() }


답변

문제는 컨테이너를 반복 할 때 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. 키가 누락 될 수있는 구조체의 모든 멤버를 선택 사항으로 선언합니다.

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

답변

속성 래퍼를 사용하여 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.


답변

필자는 @ 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)


답변

불행히도 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)")
}