Swift 4のJSONDecoderを使用すると、欠落しているキーがオプションのプロパティでなくても、デフォルト値を使用できますか?


114

Swift 4は新しいCodableプロトコルを追加しました。私が使用JSONDecoderすると、CodableクラスのオプションではないすべてのプロパティにJSONのキーが必要になるか、エラーがスローされます。

私のクラスのすべてのプロパティをオプションにすることは、私が本当に望んでいるのはjsonの値またはデフォルト値を使用することなので、不要な手間のように思えます。(このプロパティをnilにしたくありません。)

これを行う方法はありますか?

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を指定する代わりに、デフォルト値を少なくとも1つ指定する必要がある場合に実行できるもう1つのクエリ
Aditya Sharma

回答:


22

私が好むアプローチは、いわゆるDTO-データ転送オブジェクトを使用することです。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)
    }
}

この方法は、最終的なオブジェクトの名前を変更したり、好きなように変更したりできるため、優れています。それは明確であり、手動のデコードよりも少ないコードを必要とします。さらに、このアプローチでは、ネットワーク層を他のアプリから分離できます。


他のアプローチのいくつかはうまくいきましたが、最終的にはこれらの線に沿ったものが最良のアプローチだと思います。
ゼケル

よく知られていますが、コードの重複が多すぎます。私はマーティンRの答えを好む
仮面ドブレフ

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

@Hamish:最初に試したときはコンパイルされませんでしたが、現在は機能しています:)
Martin R

うん、それは(現在ビット斑状ですが、固定されますbugs.swift.org/browse/SR-5215
ハミッシュ

54
自動生成されたメソッドが非オプションからデフォルト値を読み取れないことは、まだばかげています。私は8つのオプションと1つの非オプションを持っているので、手動でエンコーダーとデコーダーの両方のメソッドを記述すると、多くの定型文が表示されます。ObjectMapperこれを非常にうまく処理します。
Legoless 2017

1
@LeoDabusあなたが準拠しDecodableていて、独自の実装も提供しているのinit(from:)でしょうか?その場合、コンパイラーは手動でデコードを処理することを想定しているため、CodingKeys列挙型を合成しません。あなたが言うように、Codable今ではコンパイラがencode(to:)あなたのために合成しているので、代わりにへの準拠が機能していますCodingKeys。の独自の実装も提供する場合encode(to:)CodingKeysは合成されなくなります。
ハミッシュ

37

1つの解決策は、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
    }
}

興味深いアプローチ。それは少しのコードを追加しますが、オブジェクトが作成された後は非常に明確で検査可能です。
Zekel

この問題に対する私のお気に入りの答え。これにより、デフォルトのJSONDecoderを引き続き使用して、1つの変数の例外を簡単に作成できます。ありがとう。
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)
    }
}

はい、これは最も明確な答えですが、大きなオブジェクトがある場合でも、多くのコードを取得します。
Ashkan Ghodrat

1

エンコードとデコードのメソッドを実装したくない場合は、デフォルト値の周りにやや汚い解決策があります。

新しいフィールドを暗黙的にラップ解除されたオプションとして宣言し、デコード後にnilかどうかを確認して、デフォルト値を設定できます。

私はこれを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からオブジェクトを初期化するために:

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

この特定の状況では、オプションを扱うことを好みますが、別の意見がある場合は、customDecode(:)メソッドをスロー可能にすることができます


0

私はまったく同じことを探してこの質問に出くわしました。私が見つけた答えは、ここでの解決策が唯一の選択肢になるのではないかと心配していましたが、あまり満足できませんでした。

私の場合、カスタムデコーダーを作成するには、維持するのが難しい大量のボイラープレートが必要になるため、他の回答を探し続けました。

私はこの記事に出くわしました。この記事は、を使用した簡単なケースでこれを克服する興味深い方法を示してい@propertyWrapperます。私にとって最も重要なことは、再利用可能であり、既存のコードのリファクタリングが最小限で済むことでした。

この記事では、不足しているブール型プロパティを失敗せずにデフォルトでfalseにしたい場合を想定していますが、他のさまざまなバリアントも示しています。より詳細に読むことができますが、私がユースケースで何をしたかを示します。

私の場合、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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.