Codable cheat sheet
Codable
var en av hörnstenarna i Swift 4.0 och medförde en otroligt smidig konvertering mellan Swift-datatyper och JSON. Det blev sedan ännu bättre i Swift 4.1 tack vare att ny funktionalitet lades till, och jag förväntar mig ännu större saker i framtiden.
I den här artikeln vill jag ge snabba kodexempel för att hjälpa dig att svara på vanliga frågor och lösa vanliga problem, allt med hjälp av Codable
.
SPONSORERAD Att bygga och underhålla infrastruktur för prenumerationer i appar är svårt. Som tur är finns det ett bättre sätt. Med RevenueCat kan du implementera prenumerationer för din app på timmar, inte månader, så att du kan återgå till att bygga din app.
Try it for free
Sponsorera Hacking with Swift och nå världens största Swift-community!
Kodning och avkodning av JSON
Låt oss börja med grunderna: konvertera lite JSON till Swift-strukturer.
För det första har vi här lite JSON att arbeta med:
let json = """"""let data = Data(json.utf8)
Den sista raden konverterar det till ett Data
-objekt eftersom det är det som Codable
-dekoder arbetar med.
Nästan måste vi definiera en Swift struct som kommer att innehålla våra färdiga data:
struct User: Codable { var name: String var age: Int}
Nu kan vi gå vidare och utföra avkodningen:
let decoder = JSONDecoder()do { let decoded = try decoder.decode(.self, from: data) print(decoded.name)} catch { print("Failed to decode JSON")}
Det kommer att skriva ut ”Paul”, vilket är namnet på den första användaren i JSON:
Konvertering av fall
Ett vanligt problem med JSON är att det kommer att använda en annan formatering för sina nyckelnamn än vad vi vill använda i Swift. Du kan till exempel få ”first_name” i din JSON och behöver konvertera det till en firstName
-egenskap.
En uppenbar lösning är att ändra antingen JSON- eller Swift-typerna så att de använder samma namngivningskonvention, men det ska vi inte göra här. Istället kommer jag att anta att du har kod som denna:
let json = """"""let data = Data(json.utf8)struct User: Codable { var firstName: String var lastName: String}
För att få det här att fungera behöver vi bara ändra en egenskap i vår JSON-dekoder:
let decoder = JSONDecoder()decoder.keyDecodingStrategy = .convertFromSnakeCase
Detta instruerar Swift att mappa namn med ormbeteckning (names_written_like_this) till namn med kamelbeteckning (namesWrittenLikeLikeThis).
Mappning av olika nyckelnamn
Om du har JSON-nycklar som är helt annorlunda än dina Swift-egenskaper kan du mappa dem med hjälp av ett CodingKeys
enum.
Ta en titt på den här JSON:
let json = """"""
Dessa nyckelnamn är inte bra, och egentligen skulle vi vilja konvertera dessa data till en struct så här:
struct User: Codable { var firstName: String var lastName: String var age: Int}
För att få det att hända måste vi deklarera en CodingKeys
enum: en mappning som Codable
kan använda för att konvertera JSON-namn till egenskaper för vår struct. Detta är en vanlig enum som använder strängar för sina råvärden så att vi kan ange både vårt egenskapsnamn (enumfallet) och JSON-namnet (enumvärdet) samtidigt. Det måste också överensstämma med CodingKey
-protokollet, vilket gör att detta fungerar med Codable
-protokollet.
Så, lägg till det här enumet i struct:
enum CodingKeys: String, CodingKey { case firstName = "user_first_name" case lastName = "user_last_name" case age}
Det kommer nu att kunna avkoda JSON som planerat.
Notera: Enummet heter CodingKeys
och protokollet heter CodingKey
.
Arbeta med ISO-8601-datum
Det finns många sätt att arbeta med datum på internet, men ISO-8601 är det vanligaste. Det kodar det fullständiga datumet i formatet ÅÅÅÅÅ-MM-DD, sedan bokstaven ”T” för att signalera att tidsinformationen börjar, sedan tiden i formatet HH:MM:SS och slutligen en tidszon. Tidszonen ”Z”, som är en förkortning av ”Zulu time”, används vanligen för att beteckna UTC.
Codable
kan hantera ISO-8601 med en inbyggd datumomvandlare. Med den här JSON:
let json = """"""
Vi kan avkoda den så här:
struct Baby: Codable { var firstName: String var timeOfBirth: Date}let decoder = JSONDecoder()decoder.keyDecodingStrategy = .convertFromSnakeCasedecoder.dateDecodingStrategy = .iso8601
Det gör det möjligt att analysera ISO-8601-datum, vilket omvandlar från 1999-04-03T17:30:31Z till en Date
-instans, samtidigt som vi hanterar konverteringen från orm- till kamelbokstav.
Arbetar med andra vanliga datum
Swift har inbyggt stöd för tre andra viktiga datumformat. Du använder dem precis som du använder ISO-8601-datum enligt ovan, så jag ska bara prata kort om dem:
- Formatet
.deferredToDate
är Apples eget datumformat, och det spårar antalet sekunder och millisekunder sedan den 1 januari 2001. Det är inte riktigt användbart utanför Apples plattformar. -
.millisecondsSince1970
-formatet anger antalet sekunder och millisekunder sedan den 1 januari 1970. Detta är ganska vanligt på nätet. - Med formatet
.secondsSince1970
anges antalet hela sekunder sedan den 1 januari 1970. Detta är extremt vanligt på nätet och är näst efter ISO-8601.
Arbeta med anpassade datum
Om ditt datumformat inte matchar något av de inbyggda alternativen behöver du inte misströsta: Codable kan analysera anpassade datum baserat på en datumformaterare som du skapar.
Den här JSON-filen spårar till exempel dagen då en student tog examen från universitetet:
let json = """"""
Det använder datumformatet DD-MM-YYYYYY, vilket inte är ett av Swifts inbyggda alternativ. Som tur är kan du tillhandahålla en förkonfigurerad DateFormatter
-instans som datumavkodningsstrategi, som här:
let formatter = DateFormatter()formatter.dateFormat = "dd-MM-yyyy"let decoder = JSONDecoder()decoder.keyDecodingStrategy = .convertFromSnakeCasedecoder.dateDecodingStrategy = .formatted(formatter)
Arbeta med konstiga datum
Ibland får du datum som är så konstiga att inte ens DateFormatter
kan hantera dem. Du kan till exempel få JSON som lagrar datum med hjälp av antalet dagar som har förflutit sedan den 1 januari 1970:
let json = """"""
För att få det att fungera måste vi skriva en egen avkodare för datumet. Allt annat kommer fortfarande att hanteras av Codable
– vi tillhandahåller bara ett anpassat avslut som behandlar datumdelen.
Du kan prova att göra lite hackig matematik här, t.ex. genom att multiplicera antalet dagar med 86400 (antalet sekunder i ett dygn) och sedan använda addTimeInterval()
-metoden i Date
. Detta tar dock inte hänsyn till sommartid och andra datumproblem, så en bättre lösning är att använda DateComponents
och Calendar
på följande sätt:
let decoder = JSONDecoder()decoder.keyDecodingStrategy = .convertFromSnakeCasedecoder.dateDecodingStrategy = .custom { decoder in // pull out the number of days from Codable let container = try decoder.singleValueContainer() let numberOfDays = try container.decode(Int.self) // create a start date of Jan 1st 1970, then a DateComponents instance for our JSON days let startDate = Date(timeIntervalSince1970: 0) var components = DateComponents() components.day = numberOfDays // create a Calendar and use it to measure the difference between the two let calendar = Calendar(identifier: .gregorian) return calendar.date(byAdding: components, to: startDate) ?? Date()}
Varning: Om du måste analysera många datum, kom ihåg att den här stängningen kommer att köras för varje enskilt datum – gör det snabbt!
Parsing hierarchical data the easy way
Alla icke-triviala JSON-filer kommer troligen att ha hierarkiska data – en samling data som är inbäddade i en annan. Till exempel:
let json = """"""
Codable
kan hantera detta utmärkt, så länge du kan beskriva relationerna tydligt.
Jag tycker att det enklaste sättet att göra detta är att använda nested structs, som här:
struct User: Codable { struct Name: Codable { var firstName: String var lastName: String } var name: Name var age: Int}
Nackdelen är att om du vill läsa en användares förnamn måste du använda user.name.firstName
, men det egentliga parsningsarbetet är i alla fall trivialt – vår befintliga kod fungerar redan!
Parsing hierarchical data the hard way
Om du vill parsa hierarkiska data till en flat struct – dvs, du vill kunna skriva user.firstName
i stället för user.name.firstName
– då måste du göra lite parsing själv. Detta är dock inte särskilt svårt, och Codable
gör det vackert typssäkert.
För det första skapar du den struct du vill ha i slutändan:
struct User: Codable { var firstName: String var lastName: String var age: Int}
För det andra måste vi definiera kodningsnycklar som beskriver var data kan hittas i hierarkin.
Vi tittar på JSON igen:
let json = """"""
Som du kan se finns det i roten en nyckel som heter ”name” och en annan som heter ”age”, så vi måste lägga till det som våra kodningsnycklar i roten. Lägg in detta i din struct:
enum CodingKeys: String, CodingKey { case name, age}
Inom ”name” fanns ytterligare två nycklar, ”first_name” och ”last_name”, så vi ska skapa några kodningsnycklar för dessa två. Lägg till detta:
enum NameCodingKeys: String, CodingKey { case firstName, lastName}
Nu kommer den svåra delen: vi måste skriva en anpassad initialiserare och en anpassad kodningsmetod för vår typ. Börja med att lägga till den här tomma metoden till din struct:
init(from decoder: Decoder) throws {}
Inom den måste vi först försöka dra fram en behållare som vi kan läsa med hjälp av nycklarna i vår CodingKeys
enum, så här:
let container = try decoder.container(keyedBy: CodingKeys.self)
När det är gjort kan vi försöka läsa vår age
egenskap. Detta görs på ett typ-säkert sätt: du talar om vilken typ du vill avkoda (Int.self
för vår ålder), tillsammans med ett nyckelnamn från CodingKeys
enum:
age = try container.decode(Int.self, forKey: .age)
Nästan måste vi gräva ner en nivå för att läsa våra namndata. Som du såg tidigare är ”name” en nyckel på högsta nivå i vårt CodingKeys
enum, men det är faktiskt en inbäddad behållare med andra värden som vi måste läsa inuti. Så vi måste dra ut den containern:
let name = try container.nestedContainer(keyedBy: NameCodingKeys.self, forKey: .name)
Och slutligen kan vi läsa två strängar för nycklarna .firstName
och .lastName
:
firstName = try name.decode(String.self, forKey: .firstName)lastName = try name.decode(String.self, forKey: .lastName)
Detta avslutar den anpassade initialiseraren, men vi har fortfarande en metod kvar att skriva: encode(to:)
. Detta är i praktiken den omvända delen av den initialiserare som vi just skrev, eftersom dess uppgift är att omvandla våra egenskaper tillbaka till nested container på lämpligt sätt:
Detta innebär att vi skapar en container baserad på vår CodingKeys
enum och skriver age
där, sedan skapar vi en nested container baserad på vår NameCodingKeys
enum och skriver både firstName
och lastName
där:
func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(age, forKey: .age) var name = container.nestedContainer(keyedBy: NameCodingKeys.self, forKey: .name) try name.encode(firstName, forKey: .firstName) try name.encode(lastName, forKey: .lastName)}
Det avslutar all vår kod. Med detta på plats kan du nu läsa firstName
-egenskapen direkt, vilket är mycket trevligare!
SPONSORED Att bygga och underhålla infrastruktur för prenumerationer i appar är svårt. Som tur är finns det ett bättre sätt. Med RevenueCat kan du implementera prenumerationer för din app på timmar, inte månader, så att du kan återgå till att bygga din app.
Try it for free
Sponsorera Hacking with Swift och nå världens största Swift-community!
Leave a Reply