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.

Hacking with Swift is sponsored by RevenueCat

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!

Hacking with Swift is sponsored by RevenueCat

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