2017-09-21 32 views
11

Khi sử dụng giao thức Swift4 và Codable tôi gặp vấn đề sau - có vẻ như không có cách nào để cho phép JSONDecoder bỏ qua các phần tử trong một mảng. Ví dụ, tôi có JSON sau:Các mảng giải mã JSONDecode không thành công nếu giải mã phần tử không thành công

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

Và một Codable struct:

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

Khi giải mã json này

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

Kết quả products trống. Điều này được mong đợi, do thực tế đối tượng thứ hai trong JSON không có khóa "points", trong khi points không phải là tùy chọn trong cấu trúc GroceryProduct.

Câu hỏi là làm cách nào để cho phép JSONDecoder "bỏ qua" đối tượng không hợp lệ?

+0

Chúng ta không thể bỏ qua các đối tượng không hợp lệ nhưng bạn có thể gán giá trị mặc định nếu nó là con số không. –

Trả lời

23

Một lựa chọn là sử dụng một loại wrapper mà cố gắng để giải mã một giá trị nhất định; lưu trữ nil nếu không thành công:

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

Sau đó chúng tôi có thể giải mã một mảng trong số này, với GroceryProduct điền của bạn trong Base giữ chỗ:

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) 
    .flatMap { $0.base } 

print(products) 

// [ 
// GroceryProduct(
//  name: "Banana", points: 200, 
//  description: Optional("A banana grown in Ecuador.") 
// ) 
// ] 

Chúng tôi sau đó sử dụng .flatMap { $0.base } để lọc ra nil yếu tố (những đã ném một lỗi về giải mã).

Điều này sẽ tạo một mảng trung gian là [FailableDecodable<GroceryProduct>], đây không phải là vấn đề; tuy nhiên nếu bạn muốn tránh nó, bạn luôn có thể tạo ra một loại wrapper rằng giải mã và unwraps mỗi phần tử từ một container unkeyed:

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

Sau đó, bạn sẽ giải mã như:

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.") 
// ) 
// ] 
+0

cảm ơn bạn đã trả lời. Tôi ngạc nhiên rằng đây là cách duy nhất ... – Remover

+0

Điều gì sẽ xảy ra nếu đối tượng cơ sở không phải là một mảng, nhưng nó có chứa một mảng? Giống như {"products": [{"name": "banana" ...}, ...]} – ludvigeriksson

+1

@ludvigeriksson Bạn chỉ muốn thực hiện giải mã trong cấu trúc đó, ví dụ: https: //gist.github .com/hamishknight/c6d270f7298e4db9e787aecb5b98bcae – Hamish

1

Thật không may Swift 4 API không có trình khởi tạo có sẵn cho init(from: Decoder).

Chỉ có một giải pháp mà tôi thấy đang triển khai giải mã tùy chỉnh, cho giá trị mặc định cho các lĩnh vực tùy chọn và có thể lọc với các dữ liệu cần thiết:

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

Có hai lựa chọn:

  1. Declare tất cả thành viên cấu trúc là tùy chọn có khóa có thể bị thiếu

    struct GroceryProduct: Codable { 
        var name: String 
        var points : Int? 
        var description: String? 
    } 
    
  2. Viết trình khởi tạo tùy chỉnh để gán các giá trị mặc định trong trường hợp 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) ?? "" 
        } 
    } 
    
+1

Thay vì 'try?' Bằng 'decode', tốt hơn nên sử dụng' try' với 'decodeIfPresent' trong tùy chọn thứ hai. Chúng ta cần đặt giá trị mặc định chỉ khi không có khóa, không phải trong trường hợp có bất kỳ lỗi giải mã nào, như khi khóa tồn tại, nhưng gõ sai. – user28434

+0

@ user28434 Tất nhiên, cảm ơn bạn. – vadian

+0

hey @vadian bạn có biết bất kỳ câu hỏi SO nào khác liên quan đến trình khởi tạo tùy chỉnh để gán giá trị mặc định trong trường hợp loại không khớp không? Tôi có một chìa khóa đó là một Int nhưng đôi khi sẽ là một String trong JSON vì vậy tôi đã cố gắng làm những gì bạn đã nói ở trên với 'deviceName = try values.decodeIfPresent (Int.self, forKey: .deviceName) ?? 00000' vì vậy nếu nó không thành công nó sẽ chỉ đặt 0000 nhưng nó vẫn không thành công. – Martheli

7

Vấn đề là khi lặp qua một vùng chứa, container.currentIndex không được tăng lên để bạn có thể thử giải mã lại bằng một loại khác.

Bởi vì currentIndex chỉ đọc, giải pháp là tự mình tăng giải mã thành công một giả. Tôi đã giải pháp @Hamish, và đã viết một wrapper với một init tùy chỉnh.

Vấn đề này là một lỗi Swift hiện tại: https://bugs.swift.org/browse/SR-5953

Các giải pháp được đăng ở đây là một cách giải quyết theo một trong các ý kiến. Tôi thích tùy chọn này vì tôi đang phân tích cú pháp một loạt các mô hình theo cùng một cách trên máy khách mạng và tôi muốn giải pháp là cục bộ cho một trong các đối tượng. Đó là, tôi vẫn muốn những người khác bị loại bỏ.

Tôi giải thích tốt hơn trong github tôi 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) 
+0

Tôi không thấy câu trả lời này, tôi đã tự mình thực hiện theo cách này và có thể đã cứu tôi những rắc rối khi tìm ra chính DummyCodable – Fraser

+1

Một biến thể, thay vì 'if/else' tôi sử dụng' do/catch' bên trong ' while' loop nên tôi có thể ghi lại lỗi – Fraser

+0

Đây phải là câu trả lời đúng. Cảm ơn bạn –

Các vấn đề liên quan