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

114

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
Зекель
источник
Еще один вопрос, что я могу сделать, если у меня есть несколько ключей в моем json, и я хочу написать общий метод для сопоставления json для создания объекта вместо того, чтобы давать nil, он должен давать как минимум значение по умолчанию.
Адитья Шарма

Ответы:

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

Этот подход также хорош, поскольку вы можете переименовывать и изменять конечный объект по своему усмотрению. Он ясен и требует меньше кода, чем ручное декодирование. Более того, с помощью этого подхода вы можете отделить сетевой уровень от другого приложения.

Леонид Сильвер
источник
Некоторые из других подходов работали нормально, но в конечном итоге я думаю, что что-то в этом роде - лучший подход.
zekel
от хорошего к известному, но слишком много дублирования кода. Я предпочитаю ответ Мартина Р.
Камен Добрев
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
    }
}

вы можете реализовать метод инициализации как

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: Он не компилировался, когда я впервые попробовал, но теперь он работает :)
Мартин Р
Да, в настоящее время это немного неоднородно, но будет исправлено ( bugs.swift.org/browse/SR-5215 )
Хэмиш
54
По-прежнему смешно, что автоматически сгенерированные методы не могут считывать значения по умолчанию из необязательных элементов. У меня есть 8 опций и 1 необязательный, поэтому теперь написание вручную методов Encoder и Decoder принесет много шаблонов. ObjectMapperсправляется с этим очень хорошо.
Legoless
1
@LeoDabus Может быть, вы соответствуете Decodableи также предоставляете свою собственную реализацию init(from:)? В этом случае компилятор предполагает, что вы хотите самостоятельно обрабатывать декодирование, и поэтому не синтезирует CodingKeysдля вас перечисление. Как вы говорите, согласование с Codableвместо этого работает, потому что теперь компилятор синтезирует encode(to:)за вас, а также синтезирует CodingKeys. Если вы также предоставите свою собственную реализацию encode(to:), CodingKeysбольше не будет синтезироваться.
Хэмиш
37

Одним из решений было бы использование вычисляемого свойства, которое по умолчанию имеет желаемое значение, если ключ 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
    }
}
Cristik
источник
Интересный подход. Он добавляет немного кода, но он очень понятен и доступен для проверки после создания объекта.
zekel 03
Мой любимый ответ на этот вопрос. Это позволяет мне по-прежнему использовать JSONDecoder по умолчанию и легко делать исключение для одной переменной. Спасибо.
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)
    }
}
Анкит
источник
да, это самый чистый ответ, но он по-прежнему получает много кода, когда у вас есть большие объекты!
Ашкан Годрат,
1

Если вы не хотите реализовывать свои методы кодирования и декодирования, есть несколько грязное решение для значений по умолчанию.

Вы можете объявить свое новое поле как неявно развернутое необязательным и проверить, не является ли оно нулевым после декодирования, и установить значение по умолчанию.

Я тестировал это только с помощью PropertyListEncoder, но я думаю, что JSONDecoder работает так же.

Кирилл Кузык
источник
1

Я столкнулся с этим вопросом в поисках того же самого. Ответы, которые я нашел, были не очень удовлетворительными, хотя я боялся, что решения здесь будут единственным вариантом.

В моем случае для создания собственного декодера потребуется тонна шаблонов, которые было бы трудно поддерживать, поэтому я продолжал искать другие ответы.

Я наткнулся на эту статью, в которой показан интересный способ решить эту проблему в простых случаях с помощью@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] = []

Надеюсь, это поможет кому-то решить ту же проблему.


ОБНОВИТЬ:

После публикации этого ответа, продолжая изучать этот вопрос, я нашел эту другую статью, но, что наиболее важно, соответствующую библиотеку, которая содержит некоторые общие простые в использовании @propertyWrappers для таких случаев:

https://github.com/marksands/BetterCodable

Lbarbosa
источник
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 (:) перекидным.

Евгений Алексеев
источник