Czy w przypadku JSONDecoder w Swift 4 brakujące klucze mogą używać wartości domyślnej zamiast być właściwościami opcjonalnymi?

114

Swift 4 dodał nowy Codableprotokół. Kiedy używam JSONDecoder, wydaje się, że wszystkie nieopcjonalne właściwości mojej Codableklasy mają klucze w formacie JSON lub generuje błąd.

Nadanie każdej właściwości mojej klasy opcjonalności wydaje się niepotrzebnym kłopotem, ponieważ naprawdę chcę użyć wartości w json lub wartości domyślnej. (Nie chcę, aby ta właściwość była zerowa).

Czy jest na to sposób?

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
zekel
źródło
Jeszcze jedno zapytanie, co mogę zrobić, jeśli mam wiele kluczy w moim jsonie i chcę napisać ogólną metodę mapowania json w celu utworzenia obiektu, zamiast dawać zero, powinno dać przynajmniej wartość domyślną.
Aditya Sharma

Odpowiedzi:

22

Podejście, które preferuję to tzw. DTO - obiekt transferu danych. Jest to struktura zgodna z Codable i reprezentująca żądany obiekt.

struct MyClassDTO: Codable {
    let items: [String]?
    let otherVar: Int?
}

Następnie po prostu inicjujesz obiekt, którego chcesz użyć w aplikacji z tym 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)
    }
}

Takie podejście jest również dobre, ponieważ możesz zmienić nazwę i zmienić ostateczny obiekt, jak chcesz. Jest przejrzysty i wymaga mniej kodu niż ręczne dekodowanie. Co więcej, dzięki temu podejściu można oddzielić warstwę sieciową od innych aplikacji.

Leonid Silver
źródło
Niektóre inne podejścia działały dobrze, ale ostatecznie myślę, że coś podobnego jest najlepszym podejściem.
zekel
dobrze wiedzieć, ale kod jest zbyt duplikowany. Wolę odpowiedź Martina R.
Kamen Dobrev
136

Możesz zaimplementować init(from decoder: Decoder)metodę w swoim typie zamiast używać domyślnej implementacji:

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

Możesz również utworzyć namestałą właściwość (jeśli chcesz):

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

lub

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

Dodaj komentarz: z niestandardowym rozszerzeniem

extension KeyedDecodingContainer {
    func decodeWrapper<T>(key: K, defaultValue: T) throws -> T
        where T : Decodable {
        return try decodeIfPresent(T.self, forKey: key) ?? defaultValue
    }
}

możesz zaimplementować metodę init jako

required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try container.decodeWrapper(key: .name, defaultValue: "Default Appleseed")
}

ale to niewiele krócej niż

    self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Default Appleseed"
Martin R.
źródło
Zwróć również uwagę, że w tym konkretnym przypadku możesz użyć automatycznie wygenerowanego CodingKeyswyliczenia (aby usunąć niestandardową definicję) :)
Hamish
@ Hamish: Nie skompilował się, kiedy po raz pierwszy go wypróbowałem, ale teraz działa :)
Martin R
Tak, obecnie jest trochę niejednolity, ale zostanie naprawiony ( bugs.swift.org/browse/SR-5215 )
Hamish
54
To nadal śmieszne, że metody generowane automatycznie nie mogą odczytać wartości domyślnych z elementów nieopcjonalnych. Mam 8 opcji i 1 nieopcjonalną, więc teraz ręczne pisanie zarówno metod kodera, jak i dekodera przyniosłoby wiele schematu. ObjectMapperradzi sobie z tym bardzo ładnie.
Bezprawny
1
@LeoDabus Czy to możliwe, że dostosowujesz się Decodablei zapewniasz również własną implementację init(from:)? W takim przypadku kompilator zakłada, że ​​chcesz ręcznie obsługiwać dekodowanie i dlatego nie syntetyzuje CodingKeysza Ciebie wyliczenia. Jak mówisz, dostosowywanie się do Codablezamiast tego działa, ponieważ teraz kompilator syntetyzuje encode(to:)za Ciebie, a więc również syntetyzuje CodingKeys. Jeśli podasz również własną implementację encode(to:), CodingKeysnie będzie już syntetyzowana.
Hamish
37

Jednym z rozwiązań byłoby użycie obliczonej właściwości, która domyślnie przyjmuje żądaną wartość, jeśli klucz JSON nie zostanie znaleziony. Dodaje to dodatkową szczegółowość, ponieważ będziesz musiał zadeklarować inną właściwość i będzie wymagać dodania CodingKeyswyliczenia (jeśli jeszcze go tam nie ma). Zaletą jest to, że nie musisz pisać niestandardowego kodu dekodowania / kodowania.

Na przykład:

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
źródło
Ciekawe podejście. Dodaje trochę kodu, ale jest bardzo przejrzysty i możliwy do sprawdzenia po utworzeniu obiektu.
zekel
Moja ulubiona odpowiedź na ten problem. Pozwala mi to nadal używać domyślnego dekodera JSOND i łatwo zrobić wyjątek dla jednej zmiennej. Dzięki.
iOS_Mouse
Uwaga: stosując to podejście, twoja właściwość staje się tylko pobierana, nie możesz przypisać wartości bezpośrednio do tej właściwości.
Ganpat
8

Możesz wdrożyć.

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)
    }
}
Ankit
źródło
tak, to jest najczystsza odpowiedź, ale nadal zawiera dużo kodu, gdy masz duże obiekty!
Ashkan Ghodrat
1

Jeśli nie chcesz implementować swoich metod kodowania i dekodowania, istnieje trochę brudne rozwiązanie dotyczące wartości domyślnych.

Możesz zadeklarować swoje nowe pole jako niejawnie rozpakowane opcjonalne i sprawdzić, czy jest zerowe po zdekodowaniu i ustawić wartość domyślną.

Przetestowałem to tylko z PropertyListEncoder, ale myślę, że JSONDecoder działa w ten sam sposób.

Kirill Kuzyk
źródło
1

Natknąłem się na to pytanie, szukając dokładnie tego samego. Odpowiedzi, które znalazłem, nie były zbyt satysfakcjonujące, mimo że obawiałem się, że rozwiązania tutaj będą jedyną opcją.

W moim przypadku stworzenie niestandardowego dekodera wymagałoby mnóstwa gotowych rozwiązań, które byłyby trudne do utrzymania, więc szukałem innych odpowiedzi.

Natrafiłem na ten artykuł, który pokazuje interesujący sposób na przezwyciężenie tego w prostych przypadkach przy użyciu pliku @propertyWrapper. Najważniejsze dla mnie było to, że był on wielokrotnego użytku i wymagał minimalnej refaktoryzacji istniejącego kodu.

W artykule przyjęto przypadek, w którym chciałbyś, aby brakująca właściwość logiczna miała domyślnie wartość false bez błędu, ale pokazuje również inne różne warianty. Możesz przeczytać to bardziej szczegółowo, ale pokażę, co zrobiłem dla mojego przypadku użycia.

W moim przypadku miałem array, że chciałem zostać zainicjowany jako pusty, jeśli brakowało klucza.

Dlatego zadeklarowałem następujące @propertyWrapperi dodatkowe rozszerzenia:

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

Zaletą tej metody jest to, że można łatwo rozwiązać problem w istniejącym kodzie, po prostu dodając @propertyWrapperdo właściwości. W moim przypadku:

@DefaultEmptyArray var items: [String] = []

Mam nadzieję, że pomoże to komuś, kto ma ten sam problem.


AKTUALIZACJA:

Po opublikowaniu tej odpowiedzi, kontynuując badanie sprawy, znalazłem ten inny artykuł, ale co najważniejsze, odpowiednią bibliotekę, która zawiera kilka popularnych, łatwych w użyciu @propertyWrapperplików do tego rodzaju przypadków:

https://github.com/marksands/BetterCodable

lbarbosa
źródło
0

Jeśli uważasz, że pisanie własnej wersji init(from decoder: Decoder)jest przytłaczające, radziłbym wdrożyć metodę, która sprawdzi wejście przed wysłaniem go do dekodera. W ten sposób będziesz mieć miejsce, w którym możesz sprawdzić brak pól i ustawić własne wartości domyślne.

Na przykład:

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

Aby zainicjować obiekt z json, zamiast:

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 będzie wyglądał następująco:

if let vuvVideoFile = PublicVideoFile.customDecode($0) {
    videos.append(vuvVideoFile)
}

W tej konkretnej sytuacji wolę mieć do czynienia z opcjami, ale jeśli masz inne zdanie, możesz sprawić, aby metoda customDecode (:) była rzucana

Eugene Alexeev
źródło