С JSONDecoder в Swift 4, могут ли отсутствующие ключи использовать значение по умолчанию вместо того, чтобы быть необязательными свойствами?

Swift 4 добавил новый Codeable протокол. Когда я использую JSONDecoder похоже, для этого требуются все необязательные свойства my Codeable класс, чтобы иметь ключи в JSON, или он выдает ошибку.

сделать каждое свойство моего класса необязательным кажется ненужной проблемой, так как я действительно хочу использовать значение в json или значение по умолчанию. (Я не хочу, чтобы собственность была нулевой.)

есть ли способ сделать это?

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

1 ответов


можно использовать тег 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"
}

Re ваш комментарий: с пользовательским расширением

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"