Swift 4'teki JSONDecoder ile, eksik anahtarlar isteğe bağlı özellikler olmak yerine varsayılan bir değer kullanabilir mi?


114

Swift 4 yeni Codableprotokolü ekledi . Kullandığımda JSONDecoder, Codablesınıfımın isteğe bağlı olmayan tüm özelliklerinin JSON'da anahtarlara sahip olmasını gerektiriyor gibi görünüyor veya bir hata veriyor.

Sınıfımın her özelliğini isteğe bağlı yapmak gereksiz bir güçlük gibi görünüyor, çünkü gerçekten istediğim şey json'daki değeri veya varsayılan bir değeri kullanmak. (Özelliğin sıfır olmasını istemiyorum.)

Bunu yapmanın bir yolu var mı?

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

Bir sorgu daha json'umda birden fazla anahtarım varsa ve nesneyi oluşturmak için json'u eşlemek için json'u eşlemek için sıfır vermek yerine en az varsayılan değer vermesi gereken genel bir yöntem yazmak istersem ne yapabilirim.
Aditya Sharma

Yanıtlar:


22

Tercih ettiğim yaklaşım DTO'lar - veri aktarım nesnesi kullanmaktır. Kodlanabilir'e uyan ve istenilen nesneyi temsil eden bir yapıdır.

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

Ardından, o DTO ile uygulamada kullanmak istediğiniz nesneyi basitçe başlatırsınız.

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

Bu yaklaşım, son nesneyi istediğiniz gibi yeniden adlandırabileceğiniz ve değiştirebileceğiniz için de iyidir. Açıktır ve manuel kod çözmeden daha az kod gerektirir. Dahası, bu yaklaşımla ağ katmanını diğer uygulamalardan ayırabilirsiniz.


Diğer yaklaşımlardan bazıları işe yaradı ama nihayetinde bu doğrultuda bir şeyin en iyi yaklaşım olduğunu düşünüyorum.
zekel

iyi biliniyor, ancak çok fazla kod kopyası var. Martin R cevabını tercih ederim
Kamen Dobrev

136

Sen uygulayabilir init(from decoder: Decoder)varsayılan uygulaması kullanarak türünüze yöntemi yerine:

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

Ayrıca namesabit bir özellik de oluşturabilirsiniz (isterseniz):

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

veya

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

Yorumunuz: Özel bir uzantıyla

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

init yöntemini şu şekilde uygulayabilirsiniz:

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

ama bu daha kısa değil

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

Ayrıca, bu özel durumda, otomatik olarak oluşturulan CodingKeysnumaralandırmayı kullanabileceğinizi unutmayın (böylece özel tanımı kaldırabilirsiniz) :)
Hamish

@Hamish: İlk denediğimde derlemedi ama şimdi çalışıyor :)
Martin R

Evet, şu anda biraz düzensiz, ancak düzeltilecek ( bugs.swift.org/browse/SR-5215 )
Hamish

54
Otomatik oluşturulan yöntemlerin isteğe bağlı olmayanlardan varsayılan değerleri okuyamaması hala saçma. 8 isteğe bağlı ve 1 isteğe bağlı olmayan seçeneğim var, bu nedenle şimdi hem Kodlayıcı hem de Kod Çözücü yöntemlerini manuel olarak yazmak çok sayıda standart şablon getirecektir. ObjectMapperbunu çok güzel hallediyor.
Legoless

1
@LeoDabus Kendinize uyuyor Decodableve kendi uygulamanızı sağlıyor olabilir init(from:)mi? Bu durumda derleyici, kod çözmeyi kendiniz elle yapmak istediğinizi varsayar ve bu nedenle CodingKeyssizin için bir enum sentezlemez . Dediğiniz gibi, Codablebunun yerine uyum sağlamak işe yarar çünkü artık derleyici encode(to:)sizin için sentezliyor ve aynı zamanda sentezliyor CodingKeys. Ayrıca kendi uygulaması sağlarsanız encode(to:), CodingKeysartık sentez edilecektir.
Hamish

37

Bir çözüm, JSON anahtarı bulunmazsa varsayılan olarak istenen değere ayarlanan hesaplanmış bir özellik kullanmaktır. Bu, başka bir özellik bildirmeniz gerekeceğinden biraz fazladan ayrıntı ekler ve CodingKeysnumaralandırmanın eklenmesini gerektirecektir (zaten orada değilse). Bunun avantajı, özel kod çözme / kodlama kodu yazmanıza gerek olmamasıdır.

Örneğin:

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

İlginç bir yaklaşım. Biraz kod ekler, ancak nesne oluşturulduktan sonra çok net ve incelenebilir.
zekel

Bu konuya en sevdiğim cevap. Hala varsayılan JSONDecoder'ı kullanmama ve bir değişken için kolayca istisna yapmama izin veriyor. Teşekkürler.
iOS_Mouse

Not: Bu yaklaşımı kullanarak mülkünüz salt gelir olur, doğrudan bu mülke değer atayamazsınız.
Ganpat

8

Uygulayabilirsiniz.

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

evet bu en temiz cevap, ancak büyük nesneleriniz olduğunda yine de çok fazla kod alıyor!
Ashkan Ghodrat

1

Kodlama ve kod çözme yöntemlerinizi uygulamak istemiyorsanız, varsayılan değerler etrafında biraz kirli bir çözüm vardır.

Yeni alanınızı örtük olarak açılmamış isteğe bağlı olarak bildirebilir ve kod çözme işleminden sonra sıfır olup olmadığını kontrol edebilir ve varsayılan bir değer ayarlayabilirsiniz.

Bunu yalnızca PropertyListEncoder ile test ettim, ancak JSONDecoder'ın aynı şekilde çalıştığını düşünüyorum.


1

Aynı şeyi arayan bu soruyla karşılaştım. Buradaki çözümlerin tek seçenek olacağından korksam da bulduğum cevaplar pek tatmin edici değildi.

Benim durumumda, özel bir kod çözücü oluşturmak, bakımı zor olan bir ton standart şablon gerektirir, bu yüzden başka cevaplar aramaya devam ettim.

A kullanarak basit durumlarda bunun üstesinden gelmenin ilginç bir yolunu gösteren bu makaleye rastladım @propertyWrapper. Benim için en önemli şey, yeniden kullanılabilir olması ve mevcut kodun minimum düzeyde yeniden düzenlenmesini gerektirmesiydi.

Makale, eksik bir boole özelliğinin başarısız olmadan varsayılan olarak yanlış olarak ayarlanmasını istediğiniz bir durumu varsayar, ancak diğer farklı varyantları da gösterir. Daha ayrıntılı olarak okuyabilirsiniz, ancak kullanım durumum için ne yaptığımı göstereceğim.

Benim durumumda, arrayanahtar eksikse boş olarak başlatılmasını istediğim bir sözüm vardı .

Bu yüzden aşağıdaki @propertyWrapperve ek uzantıları beyan ettim :

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

Bu yöntemin avantajı, yalnızca @propertyWrapperözelliğe ekleyerek mevcut koddaki sorunu kolayca çözebilmenizdir . Benim durumumda:

@DefaultEmptyArray var items: [String] = []

Umarım bu, aynı konuyla ilgilenen birine yardımcı olur.


GÜNCELLEME:

Konuyu incelemeye devam ederken bu cevabı gönderdikten sonra, bu diğer makaleyi buldum ama en önemlisi, @propertyWrapperbu tür vakalar için bazı yaygın kullanımı kolay s içeren ilgili kitaplık :

https://github.com/marksands/BetterCodable


0

Kendi versiyonunuzu yazmanın init(from decoder: Decoder)çok zor olduğunu düşünüyorsanız , girişi kod çözücüye göndermeden önce kontrol edecek bir yöntem uygulamanızı tavsiye ederim. Bu şekilde, alanların yokluğunu kontrol edebileceğiniz ve kendi varsayılan değerlerinizi belirleyebileceğiniz bir yere sahip olacaksınız.

Örneğin:

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

Ve json'dan bir nesneyi başlatmak için, bunun yerine:

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 şöyle görünecek:

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

Bu özel durumda isteğe bağlı seçeneklerle uğraşmayı tercih ederim ancak farklı bir fikriniz varsa customDecode (:) yönteminizi atılabilir hale getirebilirsiniz.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.