स्विफ्ट 4 में JSONDecoder के साथ, क्या लापता गुण वैकल्पिक गुणों के बजाय एक डिफ़ॉल्ट मान का उपयोग कर सकते हैं?


114

स्विफ्ट 4 ने नया Codableप्रोटोकॉल जोड़ा । जब मैं इसका उपयोग JSONDecoderकरता हूं तो मुझे अपनी Codableकक्षा के सभी गैर-वैकल्पिक गुणों की आवश्यकता होती है , जिसमें 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 को मैप करने के लिए ऑब्जेक्ट बनाने के बजाय nil दे सके और उसे डिफ़ॉल्ट मान कम से कम देना चाहिए।
आदित्य शर्मा

जवाबों:


22

दृष्टिकोण जिसे मैं पसंद करता हूं वह तथाकथित डीटीओ - डेटा ट्रांसफर ऑब्जेक्ट का उपयोग कर रहा है। यह एक संरचना है, जो कोडेबल के अनुरूप है और वांछित वस्तु का प्रतिनिधित्व करती है।

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

फिर आप बस उस डीटीओ के साथ ऐप में जिस ऑब्जेक्ट का उपयोग करना चाहते हैं, उसे अंकित करें।

 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 17

ज्ञात अच्छा है, लेकिन बहुत अधिक कोड दोहराव है। मैं मार्टिन आर जवाब पसंद करता हूं
कामेन डोबरेव

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

आप init पद्धति को लागू कर सकते हैं

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

@ हामिश: जब मैंने पहली बार कोशिश की थी, तब यह संकलन नहीं था, लेकिन अब यह काम करता है :)
मार्टिन आर

हाँ, यह वर्तमान में थोड़ा सा पेचीदा है , लेकिन इसे ठीक किया जाएगा ( bugs.swift.org/browse/SR-5215 )
Hamish

54
यह अभी भी हास्यास्पद है कि ऑटो-जेनरेट किए गए तरीके गैर-वैकल्पिक से डिफ़ॉल्ट मान नहीं पढ़ सकते हैं। मेरे पास 8 वैकल्पिक और 1 गैर-वैकल्पिक है, इसलिए अब मैन्युअल रूप से एनकोडर और डिकोडर दोनों तरीकों से लिखना बहुत सारे बॉयलरप्लेट लाएगा। ObjectMapperयह बहुत अच्छी तरह से संभालती है।
21

1
@LeoDabus क्या ऐसा हो सकता है कि आप इसके अनुरूप हैं Decodableऔर अपना स्वयं का कार्यान्वयन भी प्रदान कर रहे हैं init(from:)? उस स्थिति में कंपाइलर मान लेता है कि आप स्वयं डिकोडिंग को संभालना चाहते हैं और इसलिए CodingKeysआपके लिए एनम को संश्लेषित नहीं करता है । जैसा कि आप कहते हैं, Codableइसके बजाय इसके अनुरूप काम करता है क्योंकि अब संकलक encode(to:)आपके लिए संश्लेषित कर रहा है और इसलिए यह संश्लेषण भी करता है CodingKeys। यदि आप अपना खुद का कार्यान्वयन भी प्रदान करते हैं encode(to:), CodingKeysतो अब संश्लेषित नहीं किया जाएगा।
हमीश

37

एक समाधान एक गणना की गई संपत्ति का उपयोग करना होगा जो कि JSON कुंजी नहीं मिलने पर वांछित मान को डिफॉल्ट करता है। यह कुछ अतिरिक्त वर्बोसिटी जोड़ता है जैसा कि आपको एक और संपत्ति घोषित करने की आवश्यकता होगी, और CodingKeysएनम को जोड़ने की आवश्यकता होगी (यदि पहले से ही नहीं है)। लाभ यह है कि आपको कस्टम डिकोडिंग / एन्कोडिंग कोड लिखने की आवश्यकता नहीं है।

उदाहरण के लिए:

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

दिलचस्प दृष्टिकोण। इसमें थोड़ा सा कोड जोड़ा जाता है लेकिन ऑब्जेक्ट के बनने के बाद यह बहुत स्पष्ट और निरीक्षण योग्य होता है।
ज़ेकेल

इस मुद्दे पर मेरा पसंदीदा जवाब। यह मुझे अभी भी डिफ़ॉल्ट JSONDecoder का उपयोग करने और आसानी से एक चर के लिए एक अपवाद बनाने की अनुमति देता है। धन्यवाद।
iOS_Mouse

नोट: इस दृष्टिकोण का उपयोग करने से आपकी संपत्ति केवल प्राप्त होती है, आप सीधे इस संपत्ति को मूल्य नहीं दे सकते।
गणपत

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

हां यह सबसे साफ जवाब है, लेकिन यह तब भी बहुत कोड प्राप्त करता है जब आपके पास बड़ी वस्तुएं होती हैं!
अश्कान घोडरात

1

यदि आप अपने एन्कोडिंग और डिकोडिंग विधियों को लागू नहीं करना चाहते हैं, तो डिफ़ॉल्ट मानों के आसपास कुछ गंदा समाधान है।

आप अपने नए क्षेत्र को स्पष्ट रूप से अलिखित वैकल्पिक के रूप में घोषित कर सकते हैं और जांच कर सकते हैं कि यह डिकोडिंग के बाद शून्य है और डिफ़ॉल्ट मान सेट करें।

मैंने यह केवल PropertyListEncoder के साथ परीक्षण किया, लेकिन मुझे लगता है कि JSONDecoder उसी तरह काम करता है।


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 से एक वस्तु init करने के लिए:

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

इस विशेष स्थिति में मैं वैकल्पिक से निपटना पसंद करता हूं, लेकिन अगर आपकी राय अलग है, तो आप अपना कस्टमडब्लू (:) विधि फेंकने योग्य बना सकते हैं


0

मैं इस सवाल के बारे में एक ही बात की तलाश में आया था। मुझे जो उत्तर मिले, वे बहुत संतोषजनक नहीं थे, हालांकि मुझे डर था कि यहाँ समाधान एकमात्र विकल्प होगा।

मेरे मामले में, एक कस्टम डिकोडर बनाने के लिए बॉयलरप्लेट की एक टन की आवश्यकता होगी जो कि बनाए रखना मुश्किल होगा इसलिए मैं अन्य उत्तरों की खोज में रहता हूं।

मैं इस लेख में भाग गया, जो सरल मामलों में इसे दूर करने के लिए एक दिलचस्प तरीका दिखाता है @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

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.