Với JSONDecoder trong Swift 4, các khóa bị thiếu có thể sử dụng giá trị mặc định thay vì phải là thuộc tính tùy chọn không?


114

Swift 4 đã thêm Codablegiao thức mới . Khi tôi sử dụng, JSONDecodernó dường như yêu cầu tất cả các thuộc tính không phải tùy chọn của Codablelớp tôi phải có khóa trong JSON hoặc nó gây ra lỗi.

Làm cho mọi thuộc tính của lớp tôi là tùy chọn có vẻ như là một rắc rối không cần thiết vì những gì tôi thực sự muốn là sử dụng giá trị trong json hoặc một giá trị mặc định. (Tôi không muốn tài sản là con số không.)

Có cách nào để làm việc này không?

class MyCodable: Codable {
    var name: String = "Default Appleseed"
}

func load(input: String) {
    do {
        if let data = input.data(using: .utf8) {
            let result = try JSONDecoder().decode(MyCodable.self, from: data)
            print("name: \(result.name)")
        }
    } catch  {
        print("error: \(error)")
        // `Error message: "Key not found when expecting non-optional type
        // String for coding key \"name\""`
    }
}

let goodInput = "{\"name\": \"Jonny Appleseed\" }"
let badInput = "{}"
load(input: goodInput) // works, `name` is Jonny Applessed
load(input: badInput) // breaks, `name` required since property is non-optional

Một truy vấn nữa tôi có thể làm gì nếu tôi có nhiều khóa trong json của mình và tôi muốn viết một phương thức chung để ánh xạ json để tạo đối tượng thay vì cung cấp nil, nó sẽ cung cấp giá trị mặc định ít nhất.
Aditya Sharma

Câu trả lời:


22

Phương pháp tiếp cận mà tôi thích sử dụng được gọi là DTO - đối tượng truyền dữ liệu. Nó là một cấu trúc, tuân theo Codable và đại diện cho đối tượng mong muốn.

struct MyClassDTO: Codable {
    let items: [String]?
    let otherVar: Int?
}

Sau đó, bạn chỉ cần nhập đối tượng mà bạn muốn sử dụng trong ứng dụng bằng DTO đó.

 class MyClass {
    let items: [String]
    var otherVar = 3
    init(_ dto: MyClassDTO) {
        items = dto.items ?? [String]()
        otherVar = dto.otherVar ?? 3
    }

    var dto: MyClassDTO {
        return MyClassDTO(items: items, otherVar: otherVar)
    }
}

Cách tiếp cận này cũng tốt vì bạn có thể đổi tên và thay đổi đối tượng cuối cùng theo cách bạn muốn. Nó rõ ràng và cần ít mã hơn so với giải mã thủ công. Hơn nữa, với cách tiếp cận này, bạn có thể tách lớp mạng khỏi ứng dụng khác.


Một số cách tiếp cận khác hoạt động tốt nhưng cuối cùng tôi nghĩ rằng một số phương pháp dọc theo những dòng này là cách tiếp cận tốt nhất.
zekel

tốt được biết đến, nhưng có quá nhiều mã trùng lặp. Tôi thích câu trả lời của Martin R hơn
Kamen Dobrev

136

Bạn có thể triển khai init(from decoder: Decoder)phương pháp trong kiểu của mình thay vì sử dụng triển khai mặc định:

class MyCodable: Codable {
    var name: String = "Default Appleseed"

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let name = try container.decodeIfPresent(String.self, forKey: .name) {
            self.name = name
        }
    }
}

Bạn cũng có thể tạo namemột thuộc tính không đổi (nếu bạn muốn):

class MyCodable: Codable {
    let name: String

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let name = try container.decodeIfPresent(String.self, forKey: .name) {
            self.name = name
        } else {
            self.name = "Default Appleseed"
        }
    }
}

hoặc là

required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Default Appleseed"
}

Nhận xét lại của bạn: Với tiện ích mở rộng tùy chỉnh

extension KeyedDecodingContainer {
    func decodeWrapper<T>(key: K, defaultValue: T) throws -> T
        where T : Decodable {
        return try decodeIfPresent(T.self, forKey: key) ?? defaultValue
    }
}

bạn có thể triển khai phương thức init như

required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try container.decodeWrapper(key: .name, defaultValue: "Default Appleseed")
}

nhưng nó không ngắn hơn nhiều so với

    self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Default Appleseed"

Cũng lưu ý rằng trong trường hợp đặc biệt này, bạn có thể sử dụng tính năng tự động tạo ra CodingKeysđiều tra (như vậy có thể loại bỏ các định nghĩa tùy chỉnh) :)
Hamish

@Hamish: Nó không biên dịch khi tôi lần đầu tiên dùng thử, nhưng bây giờ nó hoạt động :)
Martin R

Vâng, hiện tại nó hơi loang lổ, nhưng sẽ được sửa ( bug.swift.org/browse/SR-5215 )
Hamish

54
Thật nực cười khi các phương thức được tạo tự động không thể đọc các giá trị mặc định từ các giá trị không tùy chọn. Tôi có 8 tùy chọn và 1 tùy chọn không tùy chọn, vì vậy bây giờ viết thủ công cả hai phương thức Bộ mã hóa và Bộ giải mã sẽ mang lại rất nhiều bảng soạn sẵn. ObjectMapperxử lý điều này rất tốt.
Legoless

1
@LeoDabus Có thể là bạn đang tuân theo Decodablevà cũng đang cung cấp cách triển khai của riêng bạn init(from:)không? Trong trường hợp đó, trình biên dịch giả định rằng bạn muốn tự xử lý việc giải mã theo cách thủ công và do đó không tổng hợp một CodingKeysenum cho bạn. Như bạn nói, việc tuân theo Codablethay vào đó hoạt động vì bây giờ trình biên dịch đang tổng hợp encode(to:)cho bạn và do đó cũng tổng hợp CodingKeys. Nếu bạn cũng cung cấp cách triển khai của riêng bạn encode(to:), CodingKeyssẽ không còn được tổng hợp nữa.
Hamish

37

Một giải pháp sẽ là sử dụng thuộc tính được tính toán mặc định thành giá trị mong muốn nếu không tìm thấy khóa JSON. Điều này thêm một số chi tiết bổ sung vì bạn sẽ cần khai báo một thuộc tính khác và sẽ yêu cầu thêm CodingKeysenum (nếu chưa có). Ưu điểm là bạn không cần phải viết mã giải mã / mã hóa tùy chỉnh.

Ví dụ:

class MyCodable: Codable {
    var name: String { return _name ?? "Default Appleseed" }
    var age: Int?

    private var _name: String?

    enum CodingKeys: String, CodingKey {
        case _name = "name"
        case age
    }
}

Cách tiếp cận thú vị. Nó có thêm một chút mã nhưng nó rất rõ ràng và có thể kiểm tra được sau khi đối tượng được tạo.
zekel ngày

Câu trả lời yêu thích của tôi cho vấn đề này. Nó cho phép tôi vẫn sử dụng JSONDecoder mặc định và dễ dàng tạo ngoại lệ cho một biến. Cảm ơn.
iOS_Mouse

Lưu ý: Sử dụng phương pháp này, thuộc tính của bạn trở thành chỉ nhận, bạn không thể chỉ định giá trị trực tiếp cho thuộc tính này.
Ganpat

8

Bạn có thể thực hiện.

struct Source : Codable {

    let id : String?
    let name : String?

    enum CodingKeys: String, CodingKey {
        case id = "id"
        case name = "name"
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decodeIfPresent(String.self, forKey: .id) ?? ""
        name = try values.decodeIfPresent(String.self, forKey: .name)
    }
}

vâng, đây là câu trả lời rõ ràng nhất, nhưng nó vẫn nhận được rất nhiều mã khi bạn có các đối tượng lớn!
Ashkan Ghodrat

1

Nếu bạn không muốn triển khai các phương pháp mã hóa và giải mã của mình, có một giải pháp hơi khó giải quyết xung quanh các giá trị mặc định.

Bạn có thể khai báo trường mới của mình dưới dạng tùy chọn không được bao bọc hoàn toàn và kiểm tra xem nó có phải không sau khi giải mã và đặt giá trị mặc định.

Tôi đã thử nghiệm điều này chỉ với PropertyListEncoder, nhưng tôi nghĩ JSONDecoder hoạt động theo cách tương tự.


0

Nếu bạn nghĩ rằng việc viết phiên bản của riêng bạn init(from decoder: Decoder)là quá sức, tôi khuyên bạn nên triển khai một phương pháp sẽ kiểm tra đầu vào trước khi gửi đến bộ giải mã. Bằng cách đó, bạn sẽ có một nơi mà bạn có thể kiểm tra sự vắng mặt của các trường và đặt các giá trị mặc định của riêng bạn.

Ví dụ:

final class CodableModel: Codable
{
    static func customDecode(_ obj: [String: Any]) -> CodableModel?
    {
        var validatedDict = obj
        let someField = validatedDict[CodingKeys.someField.stringValue] ?? false
        validatedDict[CodingKeys.someField.stringValue] = someField

        guard
            let data = try? JSONSerialization.data(withJSONObject: validatedDict, options: .prettyPrinted),
            let model = try? CodableModel.decoder.decode(CodableModel.self, from: data) else {
                return nil
        }

        return model
    }

    //your coding keys, properties, etc.
}

Và để bắt một đối tượng từ json, thay vì:

do {
    let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
    let model = try CodableModel.decoder.decode(CodableModel.self, from: data)                        
} catch {
    assertionFailure(error.localizedDescription)
}

Init sẽ trông như thế này:

if let vuvVideoFile = PublicVideoFile.customDecode($0) {
    videos.append(vuvVideoFile)
}

Trong tình huống cụ thể này, tôi thích xử lý các tùy chọn hơn nhưng nếu bạn có ý kiến ​​khác, bạn có thể đặt phương thức customDecode (:) của mình có thể ném được


0

Tôi đã gặp câu hỏi này để tìm kiếm điều tương tự. Các câu trả lời tôi tìm thấy không hài lòng lắm mặc dù tôi sợ rằng các giải pháp ở đây sẽ là lựa chọn duy nhất.

Trong trường hợp của tôi, việc tạo một bộ giải mã tùy chỉnh sẽ yêu cầu rất nhiều bản ghi sẵn khó duy trì nên tôi tiếp tục tìm kiếm các câu trả lời khác.

Tôi chạy vào bài viết này cho thấy một cách thú vị để khắc phục điều này trong các trường hợp đơn giản bằng cách sử dụng a @propertyWrapper. Điều quan trọng nhất đối với tôi là nó có thể tái sử dụng và yêu cầu cấu trúc lại mã hiện có ở mức tối thiểu.

Bài viết giả định trường hợp bạn muốn thuộc tính boolean bị thiếu mặc định thành false mà không bị lỗi nhưng cũng hiển thị các biến thể khác nhau. Bạn có thể đọc chi tiết hơn nhưng tôi sẽ chỉ ra những gì tôi đã làm cho trường hợp sử dụng của mình.

Trong trường hợp của tôi, tôi có một khóa arraymà tôi muốn được khởi tạo dưới dạng trống nếu thiếu khóa.

Vì vậy, tôi đã khai báo các @propertyWrapperphần mở rộng sau và các phần mở rộng bổ sung:

@propertyWrapper
struct DefaultEmptyArray<T:Codable> {
    var wrappedValue: [T] = []
}

//codable extension to encode/decode the wrapped value
extension DefaultEmptyArray: Codable {
    
    func encode(to encoder: Encoder) throws {
        try wrappedValue.encode(to: encoder)
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        wrappedValue = try container.decode([T].self)
    }
    
}

extension KeyedDecodingContainer {
    func decode<T:Decodable>(_ type: DefaultEmptyArray<T>.Type,
                forKey key: Key) throws -> DefaultEmptyArray<T> {
        try decodeIfPresent(type, forKey: key) ?? .init()
    }
}

Ưu điểm của phương pháp này là bạn có thể dễ dàng khắc phục sự cố trong mã hiện có bằng cách chỉ cần thêm thuộc tính @propertyWrappervào thuộc tính. Trong trường hợp của tôi:

@DefaultEmptyArray var items: [String] = []

Hy vọng điều này sẽ giúp ai đó đối phó với vấn đề tương tự.


CẬP NHẬT:

Sau khi đăng câu trả lời này trong khi tiếp tục xem xét vấn đề, tôi đã tìm thấy bài viết khác này nhưng quan trọng nhất là thư viện tương ứng chứa một số phổ biến dễ sử dụng @propertyWrappercho những trường hợp sau:

https://github.com/marksands/BetterCodable

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.