Подтвердить что ты не робот

С 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
4b9b3361

Ответ 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 (:) бросаемым