كيفية التعامل مع الـ JSON في الحالات النادرة بإستخدام Decodable

Apple من Swift 4 بما يعني من 2017 اضافة طريقة جديدة للتعامل مع الـ JSON عن طريق عمل mapping بإستخدام Decodable

فكل الي عليك تعمل Struct بنفس هيكلة الـ json وتمرره الى JSONDecoder.decode

مع الـ data الي جاتك من الركويست وراح يعمل mapping بشكل تلقائي

الامور بسيطه وسهله الين ما تطيح في سيناريو غير متوقع !

عشان تضبط الطريقة السابقة لازم كل القيم تكون موجوده ودائما الـ response من السيرفر يكون بنفس الـ type للكل الـ keys

بما يعني هناك عدة سيناريوهات غير مهندله

– سيناريو الـ key يكون موجود احيانا واحيانا يكون غير موجود !
– سيناريو الـ type للمتغيرات يكون مختلف مثلا مره يكون Array ومره يكون String !

في حال الـ deocde واجه احدى المشكلتين السابقة راح يعطيك error مشابهه لهذا

Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key)

لتسهل الامور راح اعمل ٣ ملفات json

الاول كل الـ keys موجوده وكلها نوعها String بإسم errors

الثاني فقط key وحده موجوده ونوعها String باسم response

الثالث كل الـ keys موجوده لكن key الـ error راح يكون dictionary بإسم errors different type

الملف الاول راح يكون بهذا الشكل

{
"result": "Any data",
"error": "Field is Fequired"
}

الـ Model راح يكون بهذا الشكل

public struct ResponseModel: Codable, Error {
var result: String
var error: String
}

لفتح الملف وتحويله الى json راح نستخدم هذا الكود

لفتح الملف وتحويله الى json
راح نستخدم هذا الكود عند تشغيل الكود راح يشتغل بدون مشاكل وراح يطبع النتيجة بدون مشاكل

do {
    guard let fileUrl = Bundle.main.url(forResource: "errors", withExtension: "json") else { fatalError() }
    
    let json = try String(contentsOf: fileUrl, encoding: String.Encoding.utf8)
    let jsonData = json.data(using: .utf8)!
        
    print(getErrorMessageString(jsonData: jsonData) ?? "")

} catch {
    print(error)
}



public struct ResponseModel: Codable, Error {

    var result: String
    var error: [String: [String]]

    public init(from decoder: Decoder) throws {
          let container = try decoder.container(keyedBy: CodingKeys.self)

        result = try container.decode(String.self, forKey: .result)
        
        if let singleError = try? container.decode(String?.self, forKey: .error) {
            error = ["error": [singleError]]
            } else {
                error = try container.decode([String:[String]]?.self, forKey: .error) ?? ["":[]]
            }

       }
}
    
    func getErrorMessageString(jsonData: Data) -> ResponseModel? {
        
        do {
            let errorMessage = try JSONDecoder().decode(ResponseModel.self, from: jsonData)
            
            return errorMessage
            
        } catch {
                print("ERROR:", error)
        }
        
        return nil
    }

لكن الان اذا غيرنا الملف الى هذا الملف

راح تحصل على هذا الخطا

{
    "result": "Any data",
}
do {
    guard let fileUrl = Bundle.main.url(forResource: "response", withExtension: "json") else { fatalError() }
    
    let json = try String(contentsOf: fileUrl, encoding: String.Encoding.utf8)
    let jsonData = json.data(using: .utf8)!
        
    print(getErrorMessageString(jsonData: jsonData) ?? "")

} catch {
    print(error)
}

المشكله هنا انه حقل error غير موجود في ملف الـ json

تقدر تحل المشكله بطريقة سهله انك تكتب Custom decode

بحيث تهندل هذا السيناريو بهذا الشكل

الي عملته اني استخدمت decodeIfPresent في حال المتغير والقيمة موجوده راح يعملها Decode غير كذا راح يعطي لها قيمة String فاضيه

public struct ResponseModel: Codable, Error {
    var result: String
    var error: String
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        result = try container.decode(String.self, forKey: .result)
        
        error = try container.decodeIfPresent(String.self, forKey: .error) ?? ""
    }
}

الان عند تشغيل المشروع ماراح تواجه مشاكل

السيناريو الاخير
اذا الملف كان يحتوى على نوع مختلف مثل هذا راح تواجه هذا الخطأ

{
    "result": "",
    "error": {
        "name": [
            "The name is required."
        ],
        "phone": [
            "The phone is required."
        ],
        "email": [
            "The email is required."
        ]
    }
}

حلها انك تحاول تعمل decode بنوع معين اذا ما زبط
تجرب تعملها بالنوع الاخر بما انه في هذا المثال مستخدم dictionary راح اغير قيمة error الى dictionary

public struct ResponseModel: Codable, Error {

    var result: String
    var error: [String: [String]]

    public init(from decoder: Decoder) throws {
          let container = try decoder.container(keyedBy: CodingKeys.self)

        result = try container.decode(String.self, forKey: .result)
        
        if let singleError = try? container.decode(String?.self, forKey: .error) {
            error = ["error": [singleError]]
            } else {
                error = try container.decode([String:[String]]?.self, forKey: .error) ?? ["":[]]
            }

       }
}

عند تشغيل المشروع لن تواجه اي مشكلة

الان لنجرب نرجع لاول ملف الي اسمه error
والـ type الـ error من نوع String

ايضا لن تواجه مشاكل، لكن النوع راح يتحول الى dictionary بسبب الكود السابق

طبعا في طرق اخرى زي تخلي النوع زي ماهو String
وبعدها تحول dictionary الى string مثلا عن طريق عمل loop واضافتها الى String جديد وعند انتهاء الـ loop ترجع الـ String

تقدر تسوي الطريقة الي تفضلها