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
가 없지만 GroceryProduct
struct 에서는 선택 사항이 아니기 때문에 예상 됩니다.
질문은 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)
답변
두 가지 옵션이 있습니다.
-
키가 누락 될 수있는 구조체의 모든 멤버를 선택 사항으로 선언합니다.
struct GroceryProduct: Codable { var name: String var points : Int? var description: String? }
-
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)")
}