web-dev-qa-db-ja.com

Swift 4のJSONDecoderで、欠落しているキーはオプションのプロパティである代わりにデフォルト値を使用できますか?

Swift 4は新しいCodeableプロトコルを追加しました。 JSONDecoderを使用すると、Codeableクラスのオプションではないプロパティすべてに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
73
zekel

デフォルトの実装を使用する代わりに、タイプに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"
92
Martin R

別の解決策は、JSONキーが見つからない場合にデフォルトで目的の値に設定される計算プロパティを使用することです。また、別のプロパティを宣言する必要があるため、さらに冗長性が追加され、CodingKeys enumを追加する必要があります(まだない場合)。利点は、カスタムのデコード/エンコードコードを記述する必要がないことです。

例えば:

class MyCodable: Codable {
    var name: String { return _name ?? "Default Appleseed" }

    private var _name: String?

    enum CodingKeys: String, CodingKey {
        case _name = "name"
    }
}
7
Cristik

私が好むアプローチは、いわゆる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)
    }
}

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

3
Leonid Silver

実装できます。

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
Ankit

独自のバージョンの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
Eugene Alexeev

エンコードとデコードの方法を実装したくない場合は、デフォルト値に関して多少汚い解決策があります。

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

これはPropertyListEncoderでのみテストしましたが、JSONDecoderは同じように機能すると思います。

0
Kirill Kuzyk