С JSONDecoder в Swift 4 могут отсутствовать клавиши использовать значение по умолчанию вместо того, чтобы быть дополнительными свойствами?
Swift 4 добавил новый протокол Codable
. Когда я использую JSONDecoder
, мне кажется, что все необязательные свойства моего класса Codable
должны иметь ключи в 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
Подход, который я предпочитаю, использует так называемые 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)
}
}
Этот подход также хорош, поскольку вы можете переименовывать и изменять конечный объект по своему усмотрению.
Это понятно и требует меньше кода, чем ручное декодирование.
Более того, при таком подходе вы можете отделить сетевой уровень от другого приложения.
Ответ 2
Вы можете реализовать метод 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"
Ответ 3
Одним из решений было бы использование вычисляемого свойства, которое по умолчанию имеет желаемое значение, если ключ 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
}
}
Ответ 4
Если вы не хотите реализовывать свои методы кодирования и декодирования, есть несколько грязное решение для значений по умолчанию.
Вы можете объявить ваше новое поле как неявно развернутое опционально и проверить, будет ли оно нулевым после декодирования, и установить значение по умолчанию.
Я проверял это только с PropertyListEncoder, но я думаю, что JSONDecoder работает так же.
Ответ 5
Вы можете реализовать.
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)
}
}
Ответ 6
Если вы думаете, что написание вашей собственной версии 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 (:) бросаемым