How to json decode linked, related items? - ios

Let's suppose this json which represents multilingual words:
[{
"id": "en_cat",
"name": "cat",
"def": "A cat is a domestic animal of the feline family.",
"trans": {
"fr": "fr_chat",
"ru": "ru_ко́шка"
}
}, {
"id": "fr_chat",
"name": "chat",
"def": "Le chat est un animal domestique de la famille des félins.",
"trans": {
"en": "en_cat",
"ru": "ru_ко́шка"
}
}, {
"id": "ru_ко́шка",
"name": "ко́шка",
"def": "..."
"trans": {
"en": "en_cat",
"fr": "fr_chat"
}
}]
This json has items related to each others in the "trans" (translation) nested container.
My class is straight forward
class Word: Decodable {
var id: String
var name: String
var definition: String
var enTranslation: Word?
var frTranslation: Word?
var ruTranslation: Word?
enum JsonCodingKey: String, CodingKey {
case id
case name
case def
case trans
}
enum JsonTransCodingKey: String, CodingKey {
case en
case fr
case ru
}
convenience init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: JsonCodingKey.self)
let id = try container.decode(String.self, forKey: .id)
let name = try container.decode(String.self, forKey: .name)
let definition = try container.decode(String.self, forKey: .def)
self.init(id: id, name: name, definition: definition)
// Tricky part here...
let transContainer = try container.nestedContainer(keyedBy: JsonTransCodingKey.self, forKey: .trans)
if let en = transContainer.decode(String.self, forKey: .en) {
self.enTranslation = realm.find(wordId: en) // Singleton that looks into memory for the word
}
// And repeat the same if logic for the other languages...
}
}
What is the fastest (CPU) way to JSON Decode it?
My way of handling it "feels" wrong:
I decode the words using
let jsonDecoder = JSONDecoder()
let words = jsonDecoder.decode([Word].self, from: data)
But the words don't have any translation linked because they are not "known" during the real-time parsing.
In my example, when we parse the first word "cat", we still don't know the French nor Russian words yet.
So I have to decode again, once I have all the words in memory.
let jsonDecoder = JSONDecoder()
let words = jsonDecoder.decode([Word].self, from: data) // Words don't have their translations
self.saveInMemory(words) // In my case, it is saved to Realm.
let words = jsonDecoder.decode([Word].self, from: data)
/* Words are now linked to each others
Because during decoding, the func Word.init(from decoder) will
look into `Realm` and find the translations. */
This double decoding feels like an overkill. Isn't there anyway to search into the json data directly?

Decode first, generate your structure later. You are trying to combine the two which does not make sense.
Your first decoding does the actual decoding, your second decoding does only the linking.
Instead of that, decode to temporary structures, build a dictionary of identifiers and use that to link them to final objects.
To be honest, there is no need to do the actual linking. It could still be completely dynamic, using a dictionary.
One possible approach:
let data = """
[{
"id": "en_cat",
"name": "cat",
"def": "A cat is a domestic animal of the feline family.",
"trans": {
"fr": "fr_chat",
"ru": "ru_ко́шка"
}
}, {
"id": "fr_chat",
"name": "chat",
"def": "Le chat est un animal domestique de la famille des félins.",
"trans": {
"en": "en_cat",
"ru": "ru_ко́шка"
}
}, {
"id": "ru_ко́шка",
"name": "ко́шка",
"def": "...",
"trans": {
"en": "en_cat",
"fr": "fr_chat"
}
}]
""".data(using: .utf8)!
enum Language: String {
case english = "en"
case french = "fr"
case russian = "ru"
}
class Word: Decodable {
let id: String
let name: String
let definition: String
let translationsIds: [String: String]
weak var parentDictionary: Dictionary!
private enum CodingKeys: String, CodingKey {
case id
case name
case definition = "def"
case translationsIds = "trans"
}
func translation(for language: Language) -> Word? {
return translationsIds[language.rawValue].flatMap { parentDictionary.words[$0] }
}
}
class Dictionary: Decodable {
let words: [String: Word]
required init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let words = try container.decode([Word].self)
self.words = [String: Word](uniqueKeysWithValues: words.map { (key: $0.id, value: $0) })
for word in words {
word.parentDictionary = self
}
}
}
let decoder = JSONDecoder()
let dictionary = try decoder.decode(Dictionary.self, from: data)
print(dictionary.words["fr_chat"]?.translation(for: .english)?.name)

Related

Custom JSON decoding: decode an array of different objects in several distinct arrays

I need your help to implement a custom JSON decoding. The JSON returned by the API is:
{
"zones": [
{
"name": "zoneA",
"blocks": [
// an array of objects of type ElementA
]
},
{
"name": "zoneB",
"blocks": [
// an array of objects of type ElementB
]
},
{
"name": "zoneC",
"blocks": [
// an array of objects of type ElementC
]
},
{
"name": "zoneD",
"blocks": [
// an array of objects of type ElementD
]
}
]
}
I don't want to parse this JSON as an array of zones with no meaning. I'd like to produce a model with an array for every specific type of block, like this:
struct Root {
let elementsA: [ElementA]
let elementsB: [ElementB]
let elementsC: [ElementC]
let elementsD: [ElementD]
}
How can I implement the Decodable protocol (by using init(from decoder:)) to follow this logic? Thank you.
This is a solution with nested containers. With the given (simplified but valid) JSON string
let jsonString = """
{
"zones": [
{
"name": "zoneA",
"blocks": [
{"name": "Foo"}
]
},
{
"name": "zoneB",
"blocks": [
{"street":"Broadway", "city":"New York"}
]
},
{
"name": "zoneC",
"blocks": [
{"email": "foo#bar.com"}
]
},
{
"name": "zoneD",
"blocks": [
{"phone": "555-01234"}
]
}
]
}
"""
and the corresponding element structs
struct ElementA : Decodable { let name: String }
struct ElementB : Decodable { let street, city: String }
struct ElementC : Decodable { let email: String }
struct ElementD : Decodable { let phone: String }
first decode the zones as nestedUnkeyedContainer then iterate the array and decode first the name key and depending on name the elements.
Side note: This way requires to declare the element arrays as variables.
struct Root : Decodable {
var elementsA = [ElementA]()
var elementsB = [ElementB]()
var elementsC = [ElementC]()
var elementsD = [ElementD]()
enum Zone: String, Decodable { case zoneA, zoneB, zoneC, zoneD }
private enum CodingKeys: String, CodingKey { case zones }
private enum ZoneCodingKeys: String, CodingKey { case name, blocks }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
var zonesContainer = try container.nestedUnkeyedContainer(forKey: .zones)
while !zonesContainer.isAtEnd {
let item = try zonesContainer.nestedContainer(keyedBy: ZoneCodingKeys.self)
let zone = try item.decode(Zone.self, forKey: .name)
switch zone {
case .zoneA: elementsA = try item.decode([ElementA].self, forKey: .blocks)
case .zoneB: elementsB = try item.decode([ElementB].self, forKey: .blocks)
case .zoneC: elementsC = try item.decode([ElementC].self, forKey: .blocks)
case .zoneD: elementsD = try item.decode([ElementD].self, forKey: .blocks)
}
}
}
}
Decoding the stuff is straightforward
do {
let result = try JSONDecoder().decode(Root.self, from: Data(jsonString.utf8))
print(result)
} catch {
print(error)
}
the "zone" property is an array of Zone objects. so you can decode them like:
enum Zone: Decodable {
case a([ElementA])
case b([ElementB])
case c([ElementC])
case d([ElementD])
enum Name: String, Codable {
case a = "zoneA"
case b = "zoneB"
case c = "zoneC"
case d = "zoneD"
}
enum RootKey: CodingKey {
case name
case blocks
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RootKey.self)
let zoneName = try container.decode(Name.self, forKey: .name)
switch zoneName {
case .a: try self = .a(container.decode([ElementA].self, forKey: .blocks))
case .b: try self = .b(container.decode([ElementB].self, forKey: .blocks))
case .c: try self = .c(container.decode([ElementC].self, forKey: .blocks))
case .d: try self = .d(container.decode([ElementD].self, forKey: .blocks))
}
}
}
Then you can filter out anything you like. For example you can pass in the array and get the result you asked in your question:
struct Root {
init(zones: [Zone]) {
elementsA = zones.reduce([]) {
guard case let .a(elements) = $1 else { return $0 }
return $0 + elements
}
elementsB = zones.reduce([]) {
guard case let .b(elements) = $1 else { return $0 }
return $0 + elements
}
elementsC = zones.reduce([]) {
guard case let .c(elements) = $1 else { return $0 }
return $0 + elements
}
elementsD = zones.reduce([]) {
guard case let .d(elements) = $1 else { return $0 }
return $0 + elements
}
}
let elementsA: [ElementA]
let elementsB: [ElementB]
let elementsC: [ElementC]
let elementsD: [ElementD]
}
✅ Benefits:
Retain the original structure (array of zones)
Handle repeating zones (if server sends more than just one for each zone)

Values of Array of struct are nil

I am pretty new to Swift and as an exercise I try to create an App with Swift 5, which should show you the Weather at a Location you searched for.
Right now I am trying to implement a function that can turn a local JSON file in a struct I am using.
The JSON file is located at the same directory as all the other files.
The JSON for testing purposes looks like this:
[
{
"id": 833,
"name": "Ḩeşār-e Sefīd",
"state": "",
"country": "IR",
"coord": {
"lon": 47.159401,
"lat": 34.330502
}
},
{
"id": 2960,
"name": "‘Ayn Ḩalāqīm",
"state": "",
"country": "SY",
"coord": {
"lon": 36.321911,
"lat": 34.940079
}
}
]
The struct:
struct Root: Codable {
let jsonWeather: [JsonWeather]?
}
struct JsonWeather: Codable {
let id: Int
let name: String
let state: String
let country: String
let coord: Coord
enum CodingKeys: String, CodingKey {
case id = "id"
case name = "name"
case state = "state"
case country = "country"
case coord = "coord"
}
}
The function that i am working on:
func loadJson(fileName: String) -> Void{
if let url = Bundle.main.url(forResource: fileName, withExtension: "json") {
do {
let data = try Data(contentsOf: url)
let jsonData = try JSONDecoder().decode(Array<Root>.self, from: data)
print(type(of: jsonData)) // Array<Root>
print(jsonData) // [WeatherApp.Root(jsonWeather: nil), WeatherApp.Root(jsonWeather: nil)]
} catch {
print("error:\(error)")
}
}
}
After all I wanted to see how the result looks like and I noticed that each jsonWeather in Root is nil
So in the end I understand that the values are nil because I allow them to be nil by setting an optional in the Root struct, but I dont quite understand why they are turning nil because there is data given. Also I don't know how I would implement it without being an optional.
This is my first time using JSONDecoder and Optionals in Swift.
Could anyone point out what I did wrong (or understood wrong) and how to fix this issue
Since your JSON is just of array of objects, you don't need a top-level struct like Root.
Get rid of your Root struct, and just decode an array of JsonWeathers:
let jsonData = try JSONDecoder().decode([JsonWeather].self, from: data)
(Note that there is no difference between [JsonWeather].self and Array<JsonWeather>.self - I just chose to do it this way since it's shorter.)

How to make Swift Codable types more versatile

I currently am working with an API that deals with bus predictions. There is an interesting quirk with the JSON that is returned for the predictions of a certain stop. When there are multiple predictions for a stop, the JSON looks something like this:
...
"direction": {
"prediction": [
{
"affectedByLayover": "true",
"block": "241",
"dirTag": "loop",
"epochTime": "1571785998536",
"isDeparture": "false",
"minutes": "20",
"seconds": "1208",
"tripTag": "121",
"vehicle": "1698"
},
{
"affectedByLayover": "true",
"block": "241",
"dirTag": "loop",
"epochTime": "1571787798536",
"isDeparture": "false",
"minutes": "50",
"seconds": "3008",
"tripTag": "122",
"vehicle": "1698"
},
{
"affectedByLayover": "true",
"block": "241",
"dirTag": "loop",
"epochTime": "1571789598536",
"isDeparture": "false",
"minutes": "80",
"seconds": "4808",
"tripTag": "123",
"vehicle": "1698"
}
],
"title": "Loop"
}
...
However, when there is only one prediction for a stop, the JSON looks like this instead:
...
"direction": {
"prediction":
{
"affectedByLayover": "true",
"block": "241",
"dirTag": "loop",
"epochTime": "1571785998536",
"isDeparture": "false",
"minutes": "20",
"seconds": "1208",
"tripTag": "121",
"vehicle": "1698"
}
"title": "Loop"
}
...
Notice that the "prediction" is no longer inside an array -- this is where I believe things are getting complicated when using a Swift Codable type to decode the JSON. My model looks like this for the "direction" and "prediction"
struct BTDirection: Codable {
let title: String!
let stopTitle: String!
let prediction: [BTPrediction]!
}
struct BTPrediction: Codable {
let minutes: String!
let vehicle: String!
}
Basically what is happening is prediction in BTDirection is looking for an Array of BTPrediction, however in the 2nd case above, this wouldn't be an Array so the decoding fails. How can I make my models more flexible to accommodate both an array or a single object? Ideally, in the 2nd case prediction would still be an array of a single BTDirection. Any help on this would be much appreciated.
You can try
struct BTDirection:Codable {
let title,stopTitle: String
let prediction: [BTPrediction]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
title = try container.decode(String.self, forKey: .title)
stopTitle = try container.decode(String.self, forKey: .stopTitle)
do {
let res = try container.decode([BTPrediction].self, forKey: .prediction)
prediction = res
}
catch {
let res = try container.decode(BTPrediction.self, forKey: .prediction)
prediction = [res]
}
}
}
To add on to Sh_Khan's answer, if you have multiple places in your API responses where this sort of thing happens, you can extract this custom decoding and encoding out to a custom wrapper type so you don't have to repeat it everywhere, like:
/// Wrapper type that can be encoded/decoded to/from either
/// an array of `Element`s or a single `Element`.
struct ArrayOrSingleItem<Element> {
private var elements: [Element]
}
extension ArrayOrSingleItem: Decodable where Element: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
// First try decoding the single value as an array of `Element`s.
elements = try container.decode([Element].self)
} catch {
// If decoding as an array of `Element`s didn't work, try decoding
// the single value as a single `Element`, and store it in an array.
elements = try [container.decode(Element.self)]
}
}
}
extension ArrayOrSingleItem: Encodable where Element: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if elements.count == 1, let element = elements.first {
// If the wrapped array of `Element`s has exactly one `Element`
// in it, encode this as just that one `Element`.
try container.encode(element)
} else {
// Otherwise, encode the wrapped array just as it is - an array
// of `Element`s.
try container.encode(elements)
}
}
}
// This lets you treat an `ArrayOrSingleItem` like a collection of elements.
// If you need the elements as type `Array<Element>`, just instantiate a new
// `Array` from your `ArrayOrSingleItem` like:
// let directions: ArrayOrSingleItem<BTDirection> = ...
// let array: [BTDirection] = Array(directions)
extension ArrayOrSingleItem: MutableCollection {
subscript(position: Int) -> Element {
get { elements[position] }
set { elements[position] = newValue }
}
var startIndex: Int { elements.startIndex }
var endIndex: Int { elements.endIndex }
func index(after i: Int) -> Int {
elements.index(after: i)
}
}
// This lets you instantiate an `ArrayOrSingleItem` from an `Array` literal.
extension ArrayOrSingleItem: ExpressibleByArrayLiteral {
init(arrayLiteral elements: Element...) {
self.elements = elements
}
}
Then you can just declare your prediction (and any other property that has the potential to be either an array or a single item in your API response) like this:
struct BTDirection: Codable {
let title: String?
let stopTitle: String?
let prediction: ArrayOrSingleItem<BTPrediction>?
}
Both #TylerTheCompiler & #Sh_Khan provide very good technical input into solution that provide the mechanics of a solution, but the provided code will hit some implementation issues with the given json data:
there are errors in the JSON posted that will stop codable working with it - I suspect these are just copy & paste errors, but if not you will have issues moving forwards.
Because of the initial direction key the JSON effectively has 3 (or at least 2.5!) layers of nesting. This will either need to be flattened in init(from:) or, as in the below, need a temporary struct for ease of mapping. Flattening in the initialiser would be more elegant, a temporary struct is far quicker :-)
CodingKeys, while obvious, isn't defined in the previous answers, so will cause errors compiling the init(from:)
there's no stopTitle field in the JSON, so that will error on decoding unless it is treated as an optional. Here I've treated it as a concrete String and handled it in the decoding; you could just make it a String? and then the decoder would cope with it being absent.
Using "corrected" JSON (added opening braces, missing commas, etc) the following code will import both scenarios. I've not implemented the arrayOrSingleItem as all credit for that belongs with #TylerTheCompiler, but you could easily drop it in.
struct Direction: Decodable {
let direction: BTDirection
}
struct BTDirection: Decodable {
enum CodingKeys: String, CodingKey {
case title
case stopTitle
case prediction
}
let prediction: [BTPrediction]
let title: String
let stopTitle: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
prediction = try container.decode([BTPrediction].self, forKey: .prediction)
} catch {
let singlePrediction = try container.decode(BTPrediction.self, forKey: .prediction)
prediction = [singlePrediction]
}
title = try container.decode(String.self, forKey: .title)
stopTitle = try container.decodeIfPresent(String.self, forKey: .stopTitle) ?? "unnamed stop"
}
}
struct BTPrediction: Decodable {
let minutes: String
let vehicle: String
}
and then to actually decode the JSON decode the top-level Direction type
let data = json.data(using: .utf8)
if let data = data {
do {
let bus = try decoder.decode(Direction.self, from: data)
// extract the BTDirection type from the temporary Direction type
// and do something with the decoded data
}catch {
//handle error
}
}
In case you're not aware, JSON Validator is very useful for validating/correcting json.

Create array of Json Data Items

I have a very long Json array that is full of items that look like this:
[
{
"id": "sm10-1",
"name": "Pheromosa & Buzzwole-GX",
"imageUrl": "https://images.pokemontcg.io/sm10/1.png",
"subtype": "TAG TEAM",
"supertype": "Pokémon",
"hp": "260",
"retreatCost": [
"Colorless",
"Colorless"
],
"convertedRetreatCost": 2,
"number": "1",
"artist": "Mitsuhiro Arita",
"rarity": "Rare Holo GX",
"series": "Sun & Moon",
"set": "Unbroken Bonds",
"setCode": "sm10",
"text": [
"When your TAG TEAM is knocked out, your opponent takes 3 Prize Cards."
],
"types": [
"Grass"
],
"attacks": [
{
"name": "Jet Punch",
"cost": [
"Grass"
],
"convertedEnergyCost": 1,
"damage": "30",
"text": "This attack does 30 damage to 1 of your opponent's Benched Pokémon. (Don't apply Weakness and Resistance for Benched Pokémon.)"
},
{
"name": "Elegant Sole",
"cost": [
"Grass",
"Grass",
"Colorless"
],
"convertedEnergyCost": 3,
"damage": "190",
"text": "During your next turn, this Pokémon's Elegant Sole attack's base damage is 60."
},
{
"name": "Beast Game-GX",
"cost": [
"Grass"
],
"convertedEnergyCost": 1,
"damage": "50",
"text": "If your opponent's Pokémon is Knocked Out by damage from this attack, take 1 more Prize card. If this Pokémon has at least 7 extra Energy attached to it (in addition to this attack's cost), take 3 more Prize cards instead. (You can't use more than 1 GX attack in a game.)"
}
],
"weaknesses": [
{
"type": "Fire",
"value": "×2"
}
],
"imageUrlHiRes": "https://images.pokemontcg.io/sm10/1_hires.png",
"nationalPokedexNumber": 794
}
]
That is just one item of hundreds in the array. What I want to do is grab specific values from each item (i.e. name, imageUrl, supertype, hp, rarity, set) and send them to a struct which will then be added to an array of such structs.
What I currently have prints just prints out all of the json data and I can not figure out how to get individual data and create an array of structs for each individual card.
Here is the code I have currently:
//[TEST] READING JSON FILE LOCALLY
struct card: Decodable {
let name: String
let imageUrl: String
let supertype: String
let artist: String
let rarity: String
let set: String
let types: Array<String>
}
func loadJsonInfo() {
do{
let data = try Data.init(contentsOf: URL.init(fileURLWithPath: Bundle.main.path(forResource: "Unbroken Bonds", ofType: "json")!))
let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
print(json)
} catch {
print(error)
}
}
Also, the json file is locally stored in my appData. Thanks in advance for your help!
Give a try to https://quicktype.io/
You put json there. And get all necessary data structures to decode json
To decode a JSON with Decodable type, you need to use JSONDecoder's decode(_:from:) method.
Update your loadJsonInfo() method to,
func loadJsonInfo() {
if let file = Bundle.main.url(forResource: "Unbroken Bonds", withExtension: "json") {
do {
let data = try Data(contentsOf: file)
let arr = try JSONDecoder().decode([Card].self, from: data)
print(arr)
} catch {
print(error)
}
}
}
Note: Use first letter capital while creating a type, i.e. use Card instead of card.
Parsing Code when you have DATA from server. I also removed force unwrapped ! so it won't crash in absence of file
func loadJsonInfo() {
if let path = Bundle.main.path(forResource: "Unbroken Bonds", ofType: "json") {
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped)
let result = try JSONDecoder().decode(ResultElement.self, from: data)
} catch let error {
print(error)
}
}
}
Your Model
import Foundation
// MARK: - ResultElement
struct ResultElement: Codable {
let id, name: String?
let imageURL: String?
let subtype, supertype, hp: String?
let retreatCost: [String]?
let convertedRetreatCost: Int?
let number, artist, rarity, series: String?
let resultSet, setCode: String?
let text, types: [String]?
let attacks: [Attack]?
let weaknesses: [Weakness]?
let imageURLHiRes: String?
let nationalPokedexNumber: Int?
enum CodingKeys: String, CodingKey {
case id, name
case imageURL = "imageUrl"
case subtype, supertype, hp, retreatCost, convertedRetreatCost, number, artist, rarity, series
case resultSet = "set"
case setCode, text, types, attacks, weaknesses
case imageURLHiRes = "imageUrlHiRes"
case nationalPokedexNumber
}
}
// MARK: - Attack
struct Attack: Codable {
let name: String?
let cost: [String]?
let convertedEnergyCost: Int?
let damage, text: String?
}
// MARK: - Weakness
struct Weakness: Codable {
let type, value: String?
}
typealias Result = [ResultElement]

JSONDecoder failed to decode nested dictionaries

I'm using JSONDecoder to decode from a JSON file which has nested dictionaries. It fails to decode from the json data to my customized model.
This is what I have tried in my code.
The JSONDecoder looks like this:
let netWorkManager = NetWorkManager(URL: url, httpMethodType: .GET)
netWorkManager.callAPI { (data, status, error) in
guard let data = data else {
onFail(NetWorkError.otherError)
return
}
switch status {
case 200:
do{
if let responseModel = try JSONDecoder().decode(ResonseModel?.self, from: data) {
onSuccess(responseModel)
}
}catch {
onFail(NetWorkError.otherError)
}
default:
onFail(NetWorkError.otherError)
}
}
The model looks like this:
struct ResonseModel: Codable {
let type : String
let format: String
let data: [String: Champion]
struct Champion: Codable {
let version: String
let id: String
let key: Int
let name: String
let title: String
let blurb: String
}
}
The JSON structure looks like this:
{
"type": "champion",
"format": "standAloneComplex",
"version": "9.3.1",
"data": {
"Aatrox": {
"version": "9.3.1",
"id": "Aatrox",
"key": "266",
"name": "Aatrox",
"title": "the Darkin Blade",
"blurb": "Once honored defenders of Shurima against the Void, Aatrox and his brethren would eventually become an even greater threat to Runeterra, and were defeated only by cunning mortal sorcery. But after centuries of imprisonment, Aatrox was the first to find...",
"info": {
"attack": 8,
"defense": 4,
"magic": 3,
"difficulty": 4
},
"tags": [
"Fighter",
"Tank"
],
"partype": "Blood Well",
},
"Ahri": {
"version": "9.3.1",
"id": "Ahri",
"key": "103",
"name": "Ahri",
"title": "the Nine-Tailed Fox",
"blurb": "Innately connected to the latent power of Runeterra, Ahri is a vastaya who can reshape magic into orbs of raw energy. She revels in toying with her prey by manipulating their emotions before devouring their life essence. Despite her predatory nature...",
"info": {
"attack": 3,
"defense": 4,
"magic": 8,
"difficulty": 5
},
"tags": [
"Mage",
"Assassin"
],
"partype": "Mana",
},
...
this is the link for the JSON if you want to look into it: http://ddragon.leagueoflegends.com/cdn/9.3.1/data/en_US/champion.json
I want to decode the "data" property as a dictionary whose key is the name of the champion and value is the champion. But the jsonDecoder seems doesn't recognize my model structure. It ends up catch the error.
The JSON-parameter "key" is not an Integer.
Change it to String and it will work:
struct ResonseModel: Codable {
let type : String
let format: String
let data: [String: Champion]
struct Champion: Codable {
let version: String
let id: String
let key: String
let name: String
let title: String
let blurb: String
}
}
You can switch to manually decoding Champion in order to clean up the data.
struct ResonseModel: Decodable {
let type : String
let format: String
let data: [String: Champion]
struct Champion: Decodable {
let version: String
let id: String
let key: Int
let name: String
let title: String
let blurb: String
enum CodingKeys: String, CodingKey {
case version, id, key, name, title, blurb
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.version = try container.decode(String.self, forKey: .version)
self.id = try container.decode(String.self, forKey: .id)
guard let key = Int(try container.decode(String.self, forKey: .key)) else {
throw DecodingError.valueNotFound(Int.self,
.init(codingPath: decoder.codingPath,
debugDescription: "Bad value for id"))
}
self.key = key
self.name = try container.decode(String.self, forKey: .name)
self.title = try container.decode(String.self, forKey: .title)
self.blurb = try container.decode(String.self, forKey: .blurb)
}
}
}
This is basically the code that the compiler writes for you; it just converts the string into an int because that's what you really wanted.

Resources