ด้วย JSONDecoder ใน Swift 4 คีย์ที่หายไปสามารถใช้ค่าเริ่มต้นแทนที่จะต้องเป็นคุณสมบัติเสริมได้หรือไม่


114

Swift 4 เพิ่มCodableโปรโตคอลใหม่ เมื่อฉันใช้JSONDecoderดูเหมือนว่าต้องการให้คุณสมบัติที่ไม่ใช่ทางเลือกทั้งหมดของCodableคลาสของฉันมีคีย์ใน JSON มิฉะนั้นจะเกิดข้อผิดพลาด

การทำให้ทุกคุณสมบัติของคลาสของฉันเป็นทางเลือกดูเหมือนจะเป็นเรื่องยุ่งยากโดยไม่จำเป็นเนื่องจากสิ่งที่ฉันต้องการจริงๆคือการใช้ค่าใน json หรือค่าเริ่มต้น (ฉันไม่ต้องการให้คุณสมบัติเป็นศูนย์)

มีวิธีทำไหม?

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

อีกหนึ่งคำถามที่ฉันสามารถทำได้ถ้าฉันมีหลายคีย์ใน json ของฉันและฉันต้องการเขียนเมธอดทั่วไปเพื่อแมป json เพื่อสร้างอ็อบเจกต์แทนที่จะให้ค่าศูนย์ควรให้ค่าเริ่มต้นอย่างน้อยที่สุด
Aditya Sharma

คำตอบ:


22

วิธีการที่ฉันชอบคือการใช้ DTOs - data transfer object เป็นโครงสร้างที่สอดคล้องกับ Codable และแสดงถึงวัตถุที่ต้องการ

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

จากนั้นคุณก็เริ่มต้นวัตถุที่คุณต้องการใช้ในแอปด้วย 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)
    }
}

วิธีนี้ก็ดีเช่นกันเนื่องจากคุณสามารถเปลี่ยนชื่อและเปลี่ยนวัตถุสุดท้ายได้ตามที่คุณต้องการ มีความชัดเจนและต้องใช้รหัสน้อยกว่าการถอดรหัสด้วยตนเอง ยิ่งไปกว่านั้นด้วยวิธีนี้คุณสามารถแยกเลเยอร์เครือข่ายออกจากแอพอื่นได้


แนวทางอื่น ๆ บางวิธีใช้ได้ผลดี แต่ในที่สุดฉันก็คิดว่าบางอย่างตามแนวเหล่านี้เป็นแนวทางที่ดีที่สุด
zekel

เป็นที่รู้จัก แต่มีการทำซ้ำรหัสมากเกินไป ฉันชอบคำตอบของ Martin R
Kamen Dobrev

136

คุณสามารถใช้init(from decoder: Decoder)วิธีการในประเภทของคุณแทนที่จะใช้การใช้งานเริ่มต้น:

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

คุณยังสามารถสร้างnameคุณสมบัติคงที่ (ถ้าคุณต้องการ):

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

หรือ

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

แสดงความคิดเห็นของคุณ:ด้วยส่วนขยายที่กำหนดเอง

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

คุณสามารถใช้วิธีการเริ่มต้นเป็น

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

แต่ก็ไม่ได้สั้นไปกว่า

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

นอกจากนี้โปรดทราบว่าในกรณีนี้คุณสามารถใช้การCodingKeysแจงนับที่สร้างขึ้นโดยอัตโนมัติ(ดังนั้นสามารถลบคำจำกัดความที่กำหนดเองได้) :)
ฮามิช

@ Hamish: มันไม่ได้รวบรวมเมื่อฉันลองครั้งแรก แต่ตอนนี้ใช้งานได้แล้ว :)
Martin R

ใช่ตอนนี้มันค่อนข้างหยาบ แต่จะได้รับการแก้ไข ( bugs.swift.org/browse/SR-5215 )
Hamish

54
ยังคงไร้สาระที่เมธอดที่สร้างขึ้นโดยอัตโนมัติไม่สามารถอ่านค่าเริ่มต้นจากตัวเลือกที่ไม่ใช่ ฉันมีตัวเลือก 8 ตัวและ 1 ตัวเลือกที่ไม่ใช่ทางเลือกดังนั้นตอนนี้การเขียนด้วยตนเองทั้งวิธีการเข้ารหัสและตัวถอดรหัสจะนำมาซึ่งต้นแบบจำนวนมาก ObjectMapperจัดการสิ่งนี้ได้ดีมาก
Legoless

1
@LeoDabus เป็นไปได้ไหมว่าคุณกำลังปฏิบัติตามDecodableและกำลังให้การดำเนินการของคุณเองด้วยinit(from:)? ในกรณีนี้คอมไพลเลอร์จะสมมติว่าคุณต้องการจัดการการถอดรหัสด้วยตัวเองดังนั้นจึงไม่สังเคราะห์CodingKeysenum ให้คุณ อย่างที่คุณพูดการปฏิบัติตามCodableแทนที่จะใช้งานได้เพราะตอนนี้คอมไพเลอร์กำลังสังเคราะห์encode(to:)ให้คุณและก็สังเคราะห์CodingKeysด้วย หากคุณยังให้การดำเนินงานของคุณเองencode(to:), CodingKeysจะไม่ถูกสังเคราะห์
Hamish

37

วิธีแก้ปัญหาหนึ่งคือการใช้คุณสมบัติที่คำนวณแล้วซึ่งตั้งค่าเริ่มต้นเป็นค่าที่ต้องการหากไม่พบคีย์ JSON สิ่งนี้จะเพิ่มความฟุ่มเฟือยพิเศษเนื่องจากคุณจะต้องประกาศคุณสมบัติอื่นและจะต้องเพิ่มCodingKeysenum (ถ้ายังไม่มี) ข้อดีคือคุณไม่จำเป็นต้องเขียนโค้ดถอดรหัส / เข้ารหัสแบบกำหนดเอง

ตัวอย่างเช่น:

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

แนวทางที่น่าสนใจ เพิ่มโค้ดเล็กน้อย แต่ชัดเจนและตรวจสอบได้หลังจากสร้างวัตถุ
zekel

คำตอบที่ฉันชอบสำหรับปัญหานี้ ช่วยให้ฉันยังคงใช้ JSONDecoder เริ่มต้นและสร้างข้อยกเว้นสำหรับตัวแปรเดียวได้อย่างง่ายดาย ขอบคุณ.
iOS_Mouse

หมายเหตุ: การใช้วิธีนี้คุณสมบัติของคุณกลายเป็นแบบ get-only คุณไม่สามารถกำหนดค่าให้กับคุณสมบัตินี้โดยตรง
Ganpat

8

คุณสามารถใช้.

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

ใช่นี่เป็นคำตอบที่ชัดเจนที่สุด แต่ก็ยังได้รับรหัสจำนวนมากเมื่อคุณมีวัตถุขนาดใหญ่!
Ashkan Ghodrat

1

หากคุณไม่ต้องการใช้วิธีการเข้ารหัสและถอดรหัสของคุณมีวิธีแก้ปัญหาที่ค่อนข้างสกปรกสำหรับค่าเริ่มต้น

คุณสามารถประกาศฟิลด์ใหม่ของคุณเป็นทางเลือกที่ไม่ได้ปิดโดยปริยายและตรวจสอบว่าเป็นศูนย์หรือไม่หลังจากถอดรหัสและตั้งค่าเริ่มต้น

ฉันทดสอบสิ่งนี้กับ PropertyListEncoder เท่านั้น แต่ฉันคิดว่า JSONDecoder ทำงานในลักษณะเดียวกัน


1

ฉันเจอคำถามนี้โดยมองหาสิ่งเดียวกัน คำตอบที่ฉันพบไม่น่าพอใจมากนักแม้ว่าฉันจะกลัวว่าคำตอบที่นี่จะเป็นทางเลือกเดียว

ในกรณีของฉันการสร้างตัวถอดรหัสที่กำหนดเองจะต้องใช้แผ่นสำเร็จรูปจำนวนมากซึ่งยากต่อการบำรุงรักษาดังนั้นฉันจึงค้นหาคำตอบอื่น ๆ ต่อไป

ฉันพบบทความนี้ซึ่งแสดงวิธีที่น่าสนใจในการเอาชนะปัญหานี้ในกรณีง่ายๆโดยใช้ไฟล์@propertyWrapper. สิ่งที่สำคัญที่สุดสำหรับฉันคือสามารถใช้ซ้ำได้และต้องการการปรับโครงสร้างโค้ดที่มีอยู่ให้น้อยที่สุด

บทความนี้สมมติว่าคุณต้องการให้คุณสมบัติบูลีนที่ขาดหายไปเป็นค่าเริ่มต้นเป็นเท็จโดยไม่ล้มเหลว แต่ยังแสดงตัวแปรอื่น ๆ คุณสามารถอ่านรายละเอียดเพิ่มเติมได้ แต่ฉันจะแสดงสิ่งที่ฉันทำสำหรับกรณีการใช้งานของฉัน

ในกรณีของฉันฉันมีสิ่งarrayที่ฉันต้องการเริ่มต้นโดยว่างเปล่าหากคีย์หายไป

ดังนั้นฉันจึงประกาศ@propertyWrapperส่วนขยายต่อไปนี้และส่วนขยายเพิ่มเติม:

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

ข้อดีของวิธีนี้คือคุณสามารถเอาชนะปัญหาในโค้ดที่มีอยู่ได้อย่างง่ายดายเพียงแค่เพิ่ม@propertyWrapperคุณสมบัติเข้าไป ในกรณีของฉัน:

@DefaultEmptyArray var items: [String] = []

หวังว่านี่จะช่วยคนที่จัดการกับปัญหาเดียวกันได้


อัพเดท:

หลังจากโพสต์คำตอบนี้ในขณะที่ตรวจสอบเรื่องนี้ต่อไปฉันพบบทความอื่น ๆ นี้แต่ที่สำคัญที่สุดคือไลบรารีที่เกี่ยวข้องซึ่งมีบางส่วนที่ใช้งานง่าย@propertyWrapperสำหรับกรณีเหล่านี้:

https://github.com/marksands/BetterCodable


0

หากคุณคิดว่าการเขียนเวอร์ชันของคุณเองinit(from decoder: Decoder)นั้นล้นหลามฉันขอแนะนำให้คุณใช้วิธีการที่จะตรวจสอบอินพุตก่อนที่จะส่งไปยังตัวถอดรหัส ด้วยวิธีนี้คุณจะมีสถานที่ที่คุณสามารถตรวจสอบการขาดฟิลด์และตั้งค่าเริ่มต้นของคุณเอง

ตัวอย่างเช่น:

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

และเพื่อเริ่มต้นวัตถุจาก json แทนที่จะเป็น:

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 จะมีลักษณะดังนี้:

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

ในสถานการณ์เฉพาะนี้ฉันต้องการจัดการกับ optionals แต่ถ้าคุณมีความคิดเห็นที่แตกต่างคุณสามารถสร้างวิธี customDecode (:) ที่สามารถโยนได้

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.