Якщо 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 для створення об’єкта, а не давати нуль, він повинен дати значення за замовчуванням як мінімум.
Адітя Шарма

Відповіді:


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

ви можете реалізувати метод 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"

Також зауважте, що в цьому конкретному випадку ви можете використовувати автоматично створене CodingKeysперерахування (так можна видалити спеціальне визначення) :)
Хаміш

@Hamish: Він не збирався, коли я вперше спробував це, але зараз він працює :)
Martin R

Так, наразі це трохи виправлено, але буде виправлено ( bugs.swift.org/browse/SR-5215 )
Hamish

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

Цікавий підхід. Він додає трохи коду, але він дуже зрозумілий і перевіряється після створення об'єкта.
зекель

Моя улюблена відповідь на це питання. Це дозволяє мені ще використовувати JSONDecoder за замовчуванням і легко робити виняток для однієї змінної. Дякую.
iOS_Mouse

Примітка. Використовуючи такий підхід, ваша власність стає лише доступною, ви не можете присвоювати їй значення безпосередньо.
Ganpat

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 працює так само.


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 (:)


0

Я натрапив на це питання, шукаючи саме те саме. Знайдені нами відповіді не надто задовольняли, хоча я боявся, що рішення тут будуть єдиним варіантом.

У моєму випадку для створення користувальницького декодера знадобилось би тонна котельня, яку важко було б підтримувати, тому я продовжував шукати інші відповіді.

Я наткнувся на цю статтю, яка показує цікавий спосіб подолати це у простих випадках за допомогою @propertyWrapper. Найголовніше для мене було те, що він був багаторазовим і потребував мінімальної рефакторингу існуючого коду.

У цій статті передбачається випадок, коли ви хочете, щоб відсутнє булеве властивість за замовчуванням було неправдивим, але не показує інші різні варіанти. Ви можете прочитати його більш детально, але я покажу, що я зробив для мого випадку використання.

У моєму випадку у мене було 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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.