
Das JSON-Format erfreut sich großer Beliebtheit und wird normalerweise für die Datenübertragung und die Ausführung von Abfragen in Client-Server-Anwendungen verwendet. Für die JSON-Analyse sind Codierungs- / Decodierungswerkzeuge dieses Formats erforderlich, die von Apple kürzlich aktualisiert wurden. In diesem Artikel werden wir uns mit JSON-Analysemethoden unter Verwendung des
Decodable- Protokolls
befassen , das neue
Codable- Protokoll mit dem Vorgänger-
NSCoding vergleichen , die Vor- und Nachteile bewerten, alles
anhand spezifischer Beispiele analysieren und auch einige Funktionen berücksichtigen, die bei der Implementierung der Protokolle auftreten.
Was ist codierbar?Auf der WWDC2017 stellte Apple zusammen mit der neuen Version von Swift 4 neue Tools zur Datencodierung / -decodierung vor, die von den folgenden drei Protokollen implementiert werden:
-
Codierbar-
Codierbar-
DekodierbarIn den meisten Fällen werden diese Protokolle für die Arbeit mit JSON verwendet. Darüber hinaus werden sie jedoch auch zum Speichern von Daten auf der Festplatte, zum Übertragen über das Netzwerk usw. verwendet. Encodable wird verwendet, um Swift-Datenstrukturen in JSON-Objekte zu konvertieren, während Decodable im Gegensatz dazu hilft, JSON-Objekte in Swift-Datenmodelle zu konvertieren. Das codierbare Protokoll kombiniert die beiden vorherigen und ist ihre Typealien:
typealias Codable = Encodable & Decodable
Um diesen Protokollen zu entsprechen, müssen Datentypen die folgenden Methoden implementieren:
Codierbarencode (to :) - codiert das Datenmodell in den angegebenen Encodertyp
Dekodierbarinit (from :) - initialisiert das Datenmodell vom bereitgestellten Decoder
Codierbarcodieren (zu :)
init (von :)
Ein einfacher AnwendungsfallBetrachten Sie nun ein einfaches Beispiel für die Verwendung von
Codable , da es sowohl
Encodable als auch
Decodable implementiert . In diesem Beispiel können Sie sofort alle Protokollfunktionen sehen. Angenommen, wir haben die einfachste JSON-Datenstruktur:
{ "title": "Nike shoes", "price": 10.5, "quantity": 1 }
Das Datenmodell für die Arbeit mit diesem JSON sieht folgendermaßen aus:
struct Product: Codable { var title:String var price:Double var quantity:Int enum CodingKeys: String, CodingKey { case title case price case quantity } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(title, forKey: .title) try container.encode(price, forKey: .price) try container.encode(quantity, forKey: .quantity) } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) title = try container.decode(String.self, forKey: .title) price = try container.decode(Double.self, forKey: .price) quantity = try container.decode(Int.self, forKey: .quantity) } }
Beide notwendigen Methoden sind implementiert, die Aufzählung wird auch beschrieben, um die Liste der Codierungs- / Decodierungsfelder zu bestimmen. Tatsächlich kann das Schreiben erheblich vereinfacht werden, da
Codable die
automatische Generierung der Methoden encode (to :) und init (from :) sowie die erforderliche Aufzählung unterstützt. Das heißt, in diesem Fall können Sie die Struktur wie folgt schreiben:
struct Product: Codable { var title:String var price:Double var quantity:Int }
Extrem einfach und minimalistisch. Vergessen Sie nur nicht, dass eine so präzise Aufnahme nicht funktioniert, wenn:
- Die
Struktur Ihres Datenmodells unterscheidet sich von der, die Sie codieren / decodieren möchten-
Möglicherweise müssen Sie neben den Eigenschaften Ihres Datenmodells weitere Eigenschaften codieren / decodieren-
Einige Eigenschaften Ihres Datenmodells unterstützen das codierbare Protokoll möglicherweise nicht. In diesem Fall müssen Sie sie vom / zum codierbaren Protokoll konvertieren-
falls die Variablennamen im Datenmodell und die Feldnamen im Container nicht mit Ihnen übereinstimmenDa wir bereits die einfachste Definition eines Datenmodells in Betracht gezogen haben, lohnt es sich, ein kleines Beispiel für seine praktische Verwendung zu geben:
In einer Zeile können Sie also die Serverantwort im JSON-Format analysieren:
let product: Product = try! JSONDecoder().decode(Product.self, for: data)
Im Gegensatz dazu erstellt der folgende Code ein JSON-Objekt aus dem Datenmodell:
let productObject = Product(title: "Cheese", price: 10.5, quantity: 1) let encodedData = try? JSONEncoder().encode(productObject)
Alles ist sehr bequem und schnell.
Nachdem Sie die Datenmodelle korrekt beschrieben und
codierbar gemacht haben , können Sie Daten buchstäblich in einer Zeile codieren / decodieren. Wir haben jedoch das einfachste Datenmodell betrachtet, das eine kleine Anzahl von Feldern eines einfachen Typs enthält. Betrachten Sie die möglichen Probleme:
Nicht alle Felder im Datenmodell sind codierbar.Damit Ihr Datenmodell das
codierbare Protokoll implementieren kann
, müssen alle Felder des Modells dieses Protokoll unterstützen. Standardmäßig unterstützt das
Codable- Protokoll die folgenden Datentypen:
String, Int, Double, Data, URL .
Codable unterstützt auch
Array, Dictionary, Optional , jedoch nur, wenn sie
codierbare Typen enthalten. Wenn einige Eigenschaften des Datenmodells nicht
Codable entsprechen,
müssen sie
dorthin gebracht werden.
struct Pet: Codable { var name: String var age: Int var type: PetType enum CodingKeys: String, CodingKey { case name case age case type } init(from decoder: Decoder) throws { . . . } func encode(to encoder: Encoder) throws { . . . } }
Wenn wir in unserem
codierbaren Datenmodell einen benutzerdefinierten Typ verwenden, z. B.
PetType , und ihn codieren / decodieren möchten, muss er auch dessen Init und Codierung implementieren.
Das Datenmodell stimmt nicht mit JSON-Feldern übereinWenn in Ihrem Datenmodell 3 Felder definiert sind und 5 Felder im JSON-Objekt zu Ihnen kommen, von denen 2 zusätzlich zu 3 sind, ändert sich beim Parsen nichts. Sie erhalten nur Ihre 3 Felder aus diesen 5. Wenn das Gegenteil passiert Situation und im JSON-Objekt gibt es mindestens ein Feld des Datenmodells, ein Laufzeitfehler wird auftreten.
Wenn einige Felder in einem JSON-Objekt optional sind und regelmäßig fehlen, müssen Sie sie in diesem Fall optional machen:
class Product: Codable { var id: Int var productTypeId: Int? var art: String var title: String var description: String var price: Double var currencyId: Int? var brandId: Int var brand: Brand? }
Verwendung komplexerer JSON-StrukturenOft ist die Serverantwort ein Array von Entitäten, dh Sie fordern beispielsweise eine Liste von Geschäften an und erhalten eine Antwort in der Form:
{ "items": [ { "id": 1, "title": " ", "link": "https://www.youtube.com/watch?v=Myp6rSeCMUw", "created_at": 1497868174, "previewImage": "http://img.youtube.com/vi/Myp6rSeCMUw/mqdefault.jpg" }, { "id": 2, "title": " 2", "link": "https://www.youtube.com/watch?v=wsCEuNJmvd8", "created_at": 1525952040, "previewImage": "http://img.youtube.com/vi/wsCEuNJmvd8/mqdefault.jpg" } ] }
In diesem Fall können Sie es einfach als Array von
Shop- Entitäten schreiben und dekodieren.
struct ShopListResponse: Decodable { enum CodingKeys: String, CodingKey { case items } let items: [Shop] }
In diesem Beispiel funktioniert die automatische Funktion
init. Wenn Sie jedoch selbst eine Dekodierung schreiben möchten, müssen Sie den dekodierten Typ als Array angeben:
self.items = try container.decode([Shop].self, forKey: .items)
Die
Shop- Struktur sollte auch das
Decodable- Protokoll implementieren
. struct Shop: Decodable { var id: Int? var title: String? var address: String? var shortAddress: String? var createdAt: Date? enum CodingKeys: String, CodingKey { case id case title case address case shortAddress = "short_address" case createdAt = "created_at" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try? container.decode(Int.self, forKey: .id) self.title = try? container.decode(String.self, forKey: .title) self.address = try? container.decode(String.self, forKey: .address) self.shortAddress = try? container.decode(String.self, forKey: .shortAddress) self.createdAt = try? container.decode(Date.self, forKey: .createdAt) } }
Das Parsen dieses Arrays von Elementen sieht folgendermaßen aus:
let parsedResult: ShopListResponse = try? JSONDecoder().decode(ShopListResponse.self, from: data)
Auf diese Weise können Sie problemlos mit Arrays von Datenmodellen arbeiten und diese in anderen Modellen verwenden.
DatumsformatIn diesem Beispiel gibt es noch eine Nuance. Hier haben wir zuerst die Verwendung des
Datums- Typs
festgestellt . Bei Verwendung dieses Typs können Probleme mit der Datumscodierung auftreten, und normalerweise stimmt dieses Problem mit dem Backend überein. Das Standardformat ist
.deferToDate :
struct MyDate : Encodable { let date: Date } let myDate = MyDate(date: Date()) try! encoder.encode(foo)
myDate sieht folgendermaßen aus:
{ "date" : 519751611.12542897 }
Wenn wir beispielsweise das
.iso8601- Format verwenden müssen, können wir das Format mithilfe der
dateEncodingStrategy- Eigenschaft einfach ändern:
encoder.dateEncodingStrategy = .iso8601
Jetzt sieht das Datum folgendermaßen aus:
{ "date" : "2017-06-21T15:29:32Z" }
Sie können auch ein benutzerdefiniertes Datumsformat verwenden oder sogar Ihren eigenen Datumsdecoder mit den folgenden Formatierungsoptionen schreiben:
.formatiert (DateFormatter) - ein eigenes
Datumsdecoderformat.custom ((Date, Encoder) throw -> Void) - Erstellen Sie Ihr eigenes
Datumsdecodierungsformat vollständig
Verschachtelte Objekte analysierenWir haben bereits untersucht, wie Sie Datenmodelle in anderen Modellen verwenden können. Manchmal ist es jedoch erforderlich, die in anderen Feldern enthaltenen JSON-Felder zu analysieren, ohne ein separates Datenmodell zu verwenden. Das Problem wird klarer, wenn wir es anhand eines Beispiels betrachten. Wir haben den folgenden JSON:
{ "id": 349, "art": "M0470500", "title": "- Vichy 50 ", "ratings": { "average_rating": 4.1034, "votes_count": 29 } }
Wir müssen die
Felder "Durchschnitt" und
"Stimmenzahl" analysieren . Dies kann auf zwei Arten gelöst werden: Erstellen Sie entweder ein Bewertungsdatenmodell mit zwei Feldern und speichern Sie die Daten darin, oder Sie können
nestedContainer verwenden . Wir haben bereits den ersten Fall besprochen, und die Verwendung des zweiten wird folgendermaßen aussehen:
class Product: Decodable { var id: Int var art: String? var title: String? var votesCount: Int var averageRating: Double enum CodingKeys: String, CodingKey { case id case art case title case ratings } enum RatingsCodingKeys: String, CodingKey { case votesCount = "votes_count" case averageRating = "average_rating" } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(Int.self, forKey: .id) self.art = try? container.decode(String.self, forKey: .art) self.title = try? container.decode(String.self, forKey: .title)
Das heißt, dieses Problem wird gelöst, indem ein weiterer zusätzlicher Container mit
nestedContainter und dessen weiterer Analyse erstellt wird. Diese Option ist praktisch, wenn die Anzahl der verschachtelten Felder nicht so groß ist. Andernfalls ist es besser, ein zusätzliches Datenmodell zu verwenden.
Nicht übereinstimmende JSON-Feldnamen und DatenmodelleigenschaftenWenn Sie darauf achten, wie die Aufzählungen in unseren Datenmodellen definiert sind, können Sie sehen, dass den Elementen der Aufzählungen manchmal eine Zeichenfolge zugewiesen wird, die den Standardwert ändert, zum Beispiel:
enum RatingsCodingKeys: String, CodingKey { case votesCount = "votes_count" case averageRating = "average_rating" }
Dies geschieht, um die Namen der Modellvariablen und JSON-Felder korrekt abzugleichen. Dies ist normalerweise für Felder erforderlich, deren Name aus mehreren Wörtern besteht, und in JSON werden sie durch Unterstriche getrennt. Im Prinzip ist eine solche Neudefinition der Aufzählung am beliebtesten und sieht einfach aus, aber selbst dann hat Apple eine elegantere Lösung gefunden. Dieses Problem kann mit
keyDecodingStrategy in einer Zeile
gelöst werden . Diese Funktion wurde in Swift 4.1 angezeigt
Angenommen, Sie haben einen JSON der Form:
let jsonString = """ [ { "name": "MacBook Pro", "screen_size": 15, "cpu_count": 4 }, { "name": "iMac Pro", "screen_size": 27, "cpu_count": 18 } ] """ let jsonData = Data(jsonString.utf8)
Erstellen wir ein Datenmodell dafür:
struct Mac: Codable { var name: String var screenSize: Int var cpuCount: Int }
Variablen im Modell werden gemäß der Vereinbarung aufgezeichnet, beginnen mit einem Kleinbuchstaben und jedes Wort beginnt mit einem Großbuchstaben (dem sogenannten
camelCase ). In JSON werden Felder jedoch mit Unterstrichen geschrieben (dem sogenannten
snake_case ). Damit das Parsen erfolgreich ist, müssen wir entweder eine Aufzählung im Datenmodell definieren, in der die Entsprechung der Namen der JSON-Felder mit den Namen der Variablen hergestellt wird, oder es wird ein Laufzeitfehler angezeigt. Jetzt ist es jedoch möglich,
keyDecodingStrategy einfach zu definieren
let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase do { let macs = try decoder.decode([Mac].self, from: jsonData) } catch { print(error.localizedDescription) }
Für die
Codierungsfunktion können Sie dementsprechend die inverse Transformation verwenden:
encoder.keyEncodingStrategy = .convertToSnakeCase
Es ist auch möglich,
keyDecodingStrategy mithilfe des folgenden Abschlusses anzupassen:
let jsonDecoder = JSONDecoder() jsonDecoder.keyDecodingStrategy = .custom { keys -> CodingKey in let key = keys.last!.stringValue.split(separator: "-").joined() return PersonKey(stringValue: String(key))! }
Dieser Eintrag ermöglicht beispielsweise die Verwendung des Trennzeichens "-" für JSON. Ein Beispiel für den verwendeten JSON:
{ "first-Name": "Taylor", "last-Name": "Swift", "age": 28 }
Daher kann eine zusätzliche Definition einer Aufzählung häufig vermieden werden.
FehlerbehandlungWenn Sie JSON analysieren und Daten von einem Format in ein anderes konvertieren, sind Fehler unvermeidlich. Schauen wir uns also die Optionen für die Behandlung verschiedener Fehlertypen an. Beim Decodieren sind folgende Fehlertypen möglich:
- DecodingError.dataCorrupted (DecodingError.Context) - Daten sind beschädigt. Normalerweise bedeutet dies, dass die Daten, die Sie dekodieren möchten, nicht mit dem erwarteten Format übereinstimmen. Beispielsweise haben Sie anstelle des erwarteten JSON ein völlig anderes Format erhalten.
- DecodingError.keyNotFound (CodingKey, DecodingError.Context) - Das angeforderte Feld wurde nicht gefunden. Bedeutet, dass das Feld, das Sie erwartet haben, fehlt
- DecodingError.typeMismatch (Any.Type, DecodingError.Context) - Typ Mismatch. Wenn der Datentyp im Modell nicht mit dem Typ des empfangenen Felds übereinstimmt
- DecodingError.valueNotFound (Any.Type, DecodingError.Context) - fehlender Wert für ein bestimmtes Feld. Das Feld, das Sie im Datenmodell definiert haben, konnte nicht initialisiert werden. Wahrscheinlich ist dieses Feld in den empfangenen Daten gleich Null. Dieser Fehler tritt nur bei nicht optionalen Feldern auf. Wenn das Feld keinen Wert haben muss, vergessen Sie nicht, es optional zu machen.
Beim Codieren von Daten ist ein Fehler möglich:
EncodingError.invalidValue (Any.Type, DecodingError.Context) - Das Datenmodell konnte nicht in ein bestimmtes Format konvertiert werden
Ein Beispiel für die Fehlerbehandlung beim Parsen von JSON:
do { let decoder = JSONDecoder() _ = try decoder.decode(businessReviewResponse.self, from: data) } catch DecodingError.dataCorrupted(let context) { print(DecodingError.dataCorrupted(context)) } catch DecodingError.keyNotFound(let key, let context) { print(DecodingError.keyNotFound(key,context)) } catch DecodingError.typeMismatch(let type, let context) { print(DecodingError.typeMismatch(type,context)) } catch DecodingError.valueNotFound(let value, let context) { print(DecodingError.valueNotFound(value,context)) } catch let error{ print(error) }
Die Fehlerverarbeitung ist natürlich besser, um eine separate Funktion einzurichten, aber hier wird aus Gründen der Klarheit die Fehleranalyse zusammen mit dem Parsen durchgeführt. Die Fehlerausgabe, wenn für das Feld "Produkt" kein Wert vorhanden ist, sieht beispielsweise folgendermaßen aus:
Vergleich von codierbarer und NSCodingNatürlich ist das codierbare Protokoll ein großer Fortschritt beim Codieren / Decodieren von Daten, aber das NSCoding-Protokoll existierte zuvor. Versuchen wir, sie zu vergleichen und herauszufinden, welche Vorteile Codable hat:
- Bei Verwendung des NSCoding- Protokolls muss das Objekt eine Unterklasse von NSObject sein , was automatisch impliziert, dass unser Datenmodell eine Klasse sein sollte. In Codable ist keine Vererbung erforderlich. Das Datenmodell kann sowohl Klasse als auch Struktur und Aufzählung sein .
- Wenn Sie separate Codierungs- und Decodierungsfunktionen benötigen, z. B. beim Parsen von über die API empfangenen JSON-Daten, können Sie nur ein decodierbares Protokoll verwenden. Das heißt, es besteht keine Notwendigkeit, die manchmal unnötigen Init- oder Codierungsmethoden zu implementieren.
- Codable kann automatisch die erforderlichen Init- und Codierungsmethoden sowie die optionale CodingKeys- Aufzählung generieren . Dies funktioniert natürlich nur, wenn Sie einfache Felder in der Datenstruktur haben, andernfalls ist eine zusätzliche Anpassung erforderlich. In den meisten Fällen, insbesondere für grundlegende Datenstrukturen, können Sie die automatische Generierung verwenden, insbesondere wenn Sie keyDecodingStrategy neu definieren . Dies ist praktisch und reduziert unnötigen Code.
Mit den
Protokollen Codable, Decodable und
Encodable konnten wir einen weiteren Schritt zur Bequemlichkeit der Datenkonvertierung unternehmen. Neue, flexiblere Parsing-Tools
wurden angezeigt, die Codemenge wurde reduziert und ein Teil der Konvertierungsprozesse wurde automatisiert. Protokolle werden nativ in Swift 4 implementiert und ermöglichen es, die Verwendung von Bibliotheken von Drittanbietern wie
SwiftyJSON zu reduzieren und gleichzeitig die Benutzerfreundlichkeit zu
gewährleisten . Protokolle ermöglichen es auch, die Codestruktur ordnungsgemäß zu organisieren, indem Datenmodelle und Methoden für die Arbeit mit ihnen in separate Module unterteilt werden.