Podczas korzystania z protokołów Swift4 i Codable napotkałem następujący problem - wygląda na to, że nie ma sposobu, aby pozwolić JSONDecoder
na pominięcie elementów w tablicy. Na przykład mam następujący kod JSON:
[
{
"name": "Banana",
"points": 200,
"description": "A banana grown in Ecuador."
},
{
"name": "Orange"
}
]
I Codable struct:
struct GroceryProduct: Codable {
var name: String
var points: Int
var description: String?
}
Podczas dekodowania tego pliku json
let decoder = JSONDecoder()
let products = try decoder.decode([GroceryProduct].self, from: json)
Wynik products
jest pusty. Czego można się spodziewać, ze względu na fakt, że drugi obiekt w JSON nie ma "points"
klucza, a points
nie jest opcjonalny w GroceryProduct
strukturze.
Pytanie brzmi, jak mogę pozwolić JSONDecoder
na „pominięcie” nieprawidłowego obiektu?
points
po prostu uznać, że jest opcjonalny?Odpowiedzi:
Jedną z opcji jest użycie typu opakowania, które próbuje zdekodować daną wartość; przechowywanie w
nil
przypadku niepowodzenia:Następnie możemy zdekodować ich tablicę,
GroceryProduct
wypełniającBase
symbol zastępczy:Następnie używamy
.compactMap { $0.base }
do odfiltrowywanianil
elementów (tych, które spowodowały błąd podczas dekodowania).Spowoduje to utworzenie pośredniej tablicy
[FailableDecodable<GroceryProduct>]
, która nie powinna stanowić problemu; jednak jeśli chcesz tego uniknąć, zawsze możesz utworzyć inny typ opakowania, który dekoduje i rozpakowuje każdy element z kontenera bez klucza:Następnie dekodowałbyś jako:
źródło
var container = try decoder.unkeyedContainer()
init(from:) throws
, więc Swift automatycznie przekaże błąd z powrotem do wywołującego (w tym przypadku dekodera, który prześle go z powrotem doJSONDecoder.decode(_:from:)
wywołania).Utworzyłbym nowy typ
Throwable
, który może opakować dowolny typ zgodny zDecodable
:Do dekodowania tablicy
GroceryProduct
(lub dowolnej innejCollection
):gdzie
value
jest obliczoną własnością wprowadzoną w rozszerzeniu naThrowable
:Wybrałbym użycie
enum
typu opakowania (powyżej aStruct
), ponieważ może być przydatne śledzenie zgłaszanych błędów, a także ich indeksów.Szybki 5
Dla Swift 5 Rozważ użycie np
Result
enum
Aby rozpakować zdekodowaną wartość, użyj
get()
metody naresult
właściwości:źródło
init
Problem polega na tym, że podczas iteracji po kontenerze element container.currentIndex nie jest zwiększany, więc możesz spróbować ponownie zdekodować z innym typem.
Ponieważ currentIndex jest tylko do odczytu, rozwiązaniem jest samodzielne zwiększenie jego wartości, pomyślne dekodowanie atrapy. Wziąłem rozwiązanie @Hamish i napisałem opakowanie z niestandardowym initem.
Ten problem jest aktualnym błędem Swift: https://bugs.swift.org/browse/SR-5953
Opublikowane tutaj rozwiązanie to obejście w jednym z komentarzy. Podoba mi się ta opcja, ponieważ analizuję kilka modeli w ten sam sposób na kliencie sieciowym i chciałem, aby rozwiązanie było lokalne dla jednego z obiektów. Oznacza to, że nadal chcę, aby pozostałe zostały odrzucone.
Lepiej wyjaśniam na moim githubie https://github.com/phynet/Lossy-array-decode-swift4
źródło
if/else
używamdo/catch
wewnątrzwhile
pętli, więc mogę zarejestrować błądIstnieją dwie możliwości:
Zadeklaruj wszystkich członków struktury jako opcjonalne, których kluczy może brakować
Napisz niestandardowy inicjator, aby przypisać domyślne wartości w
nil
przypadku.źródło
try?
zedecode
lepiej jest użyćtry
zedecodeIfPresent
w drugiej opcji. Musimy ustawić wartość domyślną tylko wtedy, gdy nie ma klucza, a nie w przypadku błędu dekodowania, na przykład gdy klucz istnieje, ale typ jest nieprawidłowy.deviceName = try values.decodeIfPresent(Int.self, forKey: .deviceName) ?? 00000
więc jeśli się nie powiedzie, po prostu wstawi 0000, ale nadal się nie powiedzie.decodeIfPresent
jest źle,API
ponieważ klucz istnieje. Użyj innegodo - catch
bloku. DekodowanieString
, jeśli wystąpi błąd, dekodowanieInt
Rozwiązanie, które umożliwił Swift 5.1, wykorzystując opakowanie właściwości:
A potem użycie:
Uwaga: Elementy opakowania właściwości będą działać tylko wtedy, gdy odpowiedź może być opakowana w strukturę (tj. Nie jest tablicą najwyższego poziomu). W takim przypadku możesz nadal zawinąć go ręcznie (aliasem typu dla lepszej czytelności):
źródło
Umieściłem rozwiązanie @ sophy-swicz, z pewnymi modyfikacjami, w łatwym w użyciu rozszerzeniu
Po prostu nazwij to tak
W powyższym przykładzie:
źródło
Niestety API Swift 4 nie ma dostępnego inicjatora dla
init(from: Decoder)
.Jedynym rozwiązaniem, które widzę, jest implementacja niestandardowego dekodowania, podając domyślną wartość dla pól opcjonalnych i możliwy filtr z potrzebnymi danymi:
źródło
Ostatnio miałem podobny problem, ale trochę inny.
W tym przypadku, jeśli jeden z elementów w
friendnamesArray
jest zerowy, cały obiekt jest zerowy podczas dekodowania.Prawidłowym sposobem obsługi tego skrajnego przypadku jest zadeklarowanie tablicy ciągów
[String]
jako tablicy opcjonalnych ciągów,[String?]
jak poniżej,źródło
Poprawiłem @ Hamish dla przypadku, w którym chcesz tego zachowania dla wszystkich tablic:
źródło
Odpowiedź @ Hamisha jest świetna. Możesz jednak zredukować
FailableCodableArray
do:źródło
Zamiast tego możesz też zrobić w ten sposób:
a potem w trakcie pobierania:
źródło
Wymyśliłem to,
KeyedDecodingContainer.safelyDecodeArray
co zapewnia prosty interfejs:Potencjalnie nieskończona pętla
while !container.isAtEnd
jest problemem i można ją rozwiązać za pomocąEmptyDecodable
.źródło
O wiele prostsza próba: dlaczego nie zadeklarujesz punktów jako opcjonalnych lub nie utworzysz tablicy zawierającej elementy opcjonalne
źródło