Können fehlende Schlüssel mit JSONDecoder in Swift 4 einen Standardwert verwenden, anstatt optionale Eigenschaften zu sein?

113

Swift 4 hat das neue CodableProtokoll hinzugefügt . Wenn ich es verwende JSONDecoder, müssen anscheinend alle nicht optionalen Eigenschaften meiner CodableKlasse Schlüssel im JSON haben, oder es wird ein Fehler ausgegeben.

Jede Eigenschaft meiner Klasse optional zu machen, scheint ein unnötiger Aufwand zu sein, da ich wirklich den Wert im json oder einen Standardwert verwenden möchte. (Ich möchte nicht, dass die Eigenschaft Null ist.)

Gibt es eine Möglichkeit, dies zu tun?

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
zekel
quelle
Noch eine Abfrage, was kann ich tun, wenn ich mehrere Schlüssel in meinem JSON habe und eine generische Methode schreiben möchte, um JSON zuzuordnen, um ein Objekt zu erstellen, anstatt Null zu geben, sollte es mindestens den Standardwert geben.
Aditya Sharma

Antworten:

21

Ein Ansatz, den ich bevorzuge, ist die Verwendung von sogenannten DTOs - Datenübertragungsobjekt. Es ist eine Struktur, die Codable entspricht und das gewünschte Objekt darstellt.

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

Dann initiieren Sie einfach das Objekt, das Sie in der App mit diesem DTO verwenden möchten.

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

Dieser Ansatz ist auch gut, da Sie das endgültige Objekt nach Belieben umbenennen und ändern können. Es ist klar und erfordert weniger Code als die manuelle Dekodierung. Darüber hinaus können Sie mit diesem Ansatz die Netzwerkschicht von anderen Apps trennen.

Leonid Silver
quelle
Einige der anderen Ansätze haben gut funktioniert, aber letztendlich denke ich, dass etwas in dieser Richtung der beste Ansatz ist.
Zekel
gut bis bekannt, aber es gibt zu viel Code-Duplizierung. Ich bevorzuge Martin R Antwort
Kamen Dobrev
136

Sie können die init(from decoder: Decoder)Methode in Ihrem Typ implementieren, anstatt die Standardimplementierung zu verwenden:

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

Sie können auch nameeine konstante Eigenschaft erstellen (wenn Sie möchten):

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

oder

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

Zu Ihrem Kommentar: Mit einer benutzerdefinierten Erweiterung

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

Sie können die init-Methode als implementieren

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

aber das ist nicht viel kürzer als

    self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Default Appleseed"
Martin R.
quelle
Beachten Sie auch, dass Sie in diesem speziellen Fall die automatisch generierte CodingKeysAufzählung verwenden können (um die benutzerdefinierte Definition zu entfernen) :)
Hamish
@ Hamish: Es wurde nicht kompiliert, als ich es zum ersten Mal ausprobiert habe, aber jetzt funktioniert es :)
Martin R.
Ja, es ist derzeit etwas lückenhaft, wird aber behoben ( bugs.swift.org/browse/SR-5215 )
Hamish
54
Es ist immer noch lächerlich, dass automatisch generierte Methoden die Standardwerte nicht von Nicht-Optionen lesen können. Ich habe 8 optionale und 1 nicht optionale Option, daher würde das manuelle Schreiben sowohl der Encoder- als auch der Decoder-Methode viel Boilerplate bringen. ObjectMapperhandhabt dies sehr gut.
Legoless
1
@LeoDabus Könnte es sein, dass Sie konform sind Decodableund auch Ihre eigene Implementierung von bereitstellen init(from:)? In diesem Fall geht der Compiler davon aus, dass Sie die Dekodierung manuell durchführen möchten, und synthetisiert daher keine CodingKeysAufzählung für Sie. Wie Sie sagen, Codablefunktioniert das Anpassen an stattdessen, weil der Compiler jetzt encode(to:)für Sie synthetisiert und so auch synthetisiert CodingKeys. Wenn Sie auch Ihre eigene Implementierung von bereitstellen encode(to:), CodingKeyswird diese nicht mehr synthetisiert.
Hamish
37

Eine Lösung wäre die Verwendung einer berechneten Eigenschaft, die standardmäßig den gewünschten Wert verwendet, wenn der JSON-Schlüssel nicht gefunden wird. Dies fügt zusätzliche Ausführlichkeit hinzu, da Sie eine andere Eigenschaft deklarieren müssen und die CodingKeysAufzählung hinzufügen müssen (falls nicht bereits vorhanden). Der Vorteil ist, dass Sie keinen benutzerdefinierten Decodierungs- / Codierungscode schreiben müssen.

Beispielsweise:

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
    }
}
Cristik
quelle
Interessanter Ansatz. Es fügt zwar ein bisschen Code hinzu, ist aber nach dem Erstellen des Objekts sehr klar und überprüfbar.
Zekel
Meine Lieblingsantwort auf dieses Problem. Damit kann ich weiterhin den Standard-JSONDecoder verwenden und problemlos eine Ausnahme für eine Variable machen. Vielen Dank.
iOS_Mouse
Hinweis: Mit diesem Ansatz wird Ihre Eigenschaft nur noch get verfügbar. Sie können dieser Eigenschaft keinen Wert direkt zuweisen.
Ganpat
8

Sie können implementieren.

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)
    }
}
Ankit
quelle
Ja, das ist die sauberste Antwort, aber es bekommt immer noch viel Code, wenn Sie große Objekte haben!
Ashkan Ghodrat vor
1

Wenn Sie Ihre Codierungs- und Decodierungsmethoden nicht implementieren möchten, gibt es eine etwas schmutzige Lösung für Standardwerte.

Sie können Ihr neues Feld als implizit entpackt optional deklarieren und nach dem Dekodieren prüfen, ob es Null ist, und einen Standardwert festlegen.

Ich habe dies nur mit PropertyListEncoder getestet, aber ich denke, JSONDecoder funktioniert genauso.

Kirill Kuzyk
quelle
0

Wenn Sie der Meinung sind, dass das Schreiben Ihrer eigenen Version von init(from decoder: Decoder)überwältigend ist, würde ich Ihnen raten, eine Methode zu implementieren, die die Eingabe überprüft, bevor Sie sie an den Decoder senden. Auf diese Weise haben Sie einen Ort, an dem Sie nach fehlenden Feldern suchen und Ihre eigenen Standardwerte festlegen können.

Beispielsweise:

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

Und um ein Objekt von json aus zu initiieren, anstatt:

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 wird so aussehen:

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

In dieser speziellen Situation beschäftige ich mich lieber mit Optionen, aber wenn Sie eine andere Meinung haben, können Sie Ihre customDecode (:) -Methode werfen lassen

Eugene Alexeev
quelle
0

Ich bin auf diese Frage gestoßen und habe genau das Gleiche gesucht. Die Antworten, die ich fand, waren nicht sehr zufriedenstellend, obwohl ich befürchtete, dass die Lösungen hier die einzige Option sein würden.

In meinem Fall würde das Erstellen eines benutzerdefinierten Decoders eine Menge Boilerplate erfordern, die schwer zu warten wäre, sodass ich weiter nach anderen Antworten suchte.

Ich bin auf diesen Artikel gestoßen, der einen interessanten Weg zeigt, dies in einfachen Fällen mit a zu überwinden @propertyWrapper. Das Wichtigste für mich war, dass es wiederverwendbar war und nur minimales Refactoring des vorhandenen Codes erforderte.

Der Artikel geht von einem Fall aus, in dem eine fehlende boolesche Eigenschaft standardmäßig auf false gesetzt werden soll, ohne dass dies fehlschlägt, zeigt aber auch andere unterschiedliche Varianten an. Sie können es genauer lesen, aber ich werde zeigen, was ich für meinen Anwendungsfall getan habe.

In meinem Fall hatte ich eine array, die ich als leer initialisieren wollte, wenn der Schlüssel fehlte.

Also habe ich die folgenden @propertyWrapperund zusätzliche Erweiterungen deklariert :

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

Der Vorteil dieser Methode besteht darin, dass Sie das Problem im vorhandenen Code leicht beheben können, indem Sie das einfach @propertyWrapperzur Eigenschaft hinzufügen . In meinem Fall:

@DefaultEmptyArray var items: [String] = []

Ich hoffe, dies hilft jemandem, der sich mit dem gleichen Problem befasst.


AKTUALISIEREN:

Nachdem ich diese Antwort gepostet hatte, während ich mich weiter mit der Angelegenheit befasste, fand ich diesen anderen Artikel, aber vor allem die jeweilige Bibliothek, die einige gängige, einfach zu verwendende @propertyWrappers für diese Art von Fällen enthält:

https://github.com/marksands/BetterCodable

lbarbosa
quelle