Decoding JSON is a common task, and with the improvements in Swift has become much easier. Decoding nested JSON can still be somewhat complex, and in this example we’ll take data from the Free Fortnite API and use that to illustrate how to parse nested JSON data in Swift.
To begin with we’re going to import the foundation library for JSONDecoder and create a string containing the json data (the current data can be found at https://fortnite-public-api.theapinetwork.com/prod09/challenges/get?season=current).
// create the json string, this from fortniteapi.com containing
// the Fortnite challenges data
let json = "{"language":"en","season":7,"currentweek":7,"star":"https:\\/\\/fortnite–public–files.theapinetwork.com\\/fortnite–br–challenges–star.png","challenges":{"week1":[{"identifier":"e2c420d–928d4bf–8ce0ff2–ec19b37","challenge":"Pick up an item of each rarity","total":5,"stars":5,"difficulty":"normal"},{"identifier":"e2c420d–928d4bf–8ce0ff2–ec19b37","challenge":"Dance in different forbidden locations","total":7,"stars":5,"difficulty":"normal"},{"identifier":"e2c420d–928d4bf–8ce0ff2–ec19b37","challenge":"Play matches with at least one elimination","total":5,"stars":10,"difficulty":"hard"},{"identifier":"e2c420d–928d4bf–8ce0ff2–ec19b37","challenge":"Stage 1: Dance on top of a crown of RV\’s","total":1,"stars":5,"difficulty":"normal"},{"identifier":"e2c420d–928d4bf–8ce0ff2–ec19b37","challenge":"Deal Headshot Damage to opponents","total":500,"stars":5,"difficulty":"normal"},{"identifier":"e2c420d–928d4bf–8ce0ff2–ec19b37","challenge":"Stage 1: Search Ammo Boxes in a single match","total":5,"stars":10,"difficulty":"hard"},{"identifier":"e2c420d–928d4bf–8ce0ff2–ec19b37","challenge":"Eliminate opponents in different Named Locations","total":5,"stars":10,"difficulty":"hard"}],"week2":[{"identifier":"32bb90e–8976aab–5298d5d–a10fe66","challenge":"Search a Chest in different Named Locations","total":7,"stars":5,"difficulty":"normal"},{"identifier":"32bb90e–8976aab–5298d5d–a10fe66","challenge":"Damage opponents with different types of weapons","total":5,"stars":5,"difficulty":"normal"},{"identifier":"32bb90e–8976aab–5298d5d–a10fe66","challenge":"Eliminate opponents in Snobby Shores or Fatal Fields","total":3,"stars":10,"difficulty":"hard"},{"identifier":"32bb90e–8976aab–5298d5d–a10fe66","challenge":"Stage 1: Visit Snobby Shores and Pleasant Park in a single match","total":2,"stars":5,"difficulty":"normal"},{"identifier":"32bb90e–8976aab–5298d5d–a10fe66","challenge":"Play the Sheet Music on the planos near Pleasant Park and Lonely Lodge","total":2,"stars":5,"difficulty":"normal"},{"identifier":"32bb90e–8976aab–5298d5d–a10fe66","challenge":"Complete in a Dance Off at an abandoned mansion","total":1,"stars":10,"difficulty":"hard"},{"identifier":"32bb90e–8976aab–5298d5d–a10fe66","challenge":"Eliminate an opponents from at least 50m away","total":1,"stars":10,"difficulty":"hard"}],"week3":[{"identifier":"d2ddea1–8f00665–ce8623e–36bd4e3","challenge":"Ride a Zipline in different matches","total":5,"stars":5,"difficulty":"normal"},{"identifier":"d2ddea1–8f00665–ce8623e–36bd4e3","challenge":"Stage 1: Land at Lonely Lodge","total":1,"stars":1,"difficulty":"normal"},{"identifier":"d2ddea1–8f00665–ce8623e–36bd4e3","challenge":"Legendary weapon Eliminations","total":2,"stars":10,"difficulty":"hard"},{"identifier":"d2ddea1–8f00665–ce8623e–36bd4e3","challenge":"Search Chests at Polar Peak or Tomato Temple","total":7,"stars":5,"difficulty":"normal"},{"identifier":"d2ddea1–8f00665–ce8623e–36bd4e3","challenge":"Ring a doorbell in different named locations in a single match","total":2,"stars":5,"difficulty":"normal"},{"identifier":"d2ddea1–8f00665–ce8623e–36bd4e3","challenge":"Search between three ski lodges","total":1,"stars":10,"difficulty":"hard"},{"identifier":"d2ddea1–8f00665–ce8623e–36bd4e3","challenge":"Stage 1: Deal damage with Shotguns to opponents","total":200,"stars":3,"difficulty":"normal"}],"week4":[{"identifier":"ad61ab1–43223ef–bc24c7d–2583be6","challenge":"Use an X–4 Stormwing plane in different matches","total":5,"stars":5,"difficulty":"normal"},{"identifier":"ad61ab1–43223ef–bc24c7d–2583be6","challenge":"Launch fireworks","total":3,"stars":5,"difficulty":"normal"},{"identifier":"ad61ab1–43223ef–bc24c7d–2583be6","challenge":"Eliminate opponents at Expedition Outposts","total":3,"stars":10,"difficulty":"hard"},{"identifier":"ad61ab1–43223ef–bc24c7d–2583be6","challenge":"Stage 1: Destroy chairs","total":80,"stars":2,"difficulty":"normal"},{"identifier":"ad61ab1–43223ef–bc24c7d–2583be6","challenge":"Deal damage with a pickaxe to opponents","total":100,"stars":5,"difficulty":"normal"},{"identifier":"ad61ab1–43223ef–bc24c7d–2583be6","challenge":"Eliminate opponents at Happy Hamlet or Pleasent Park","total":3,"stars":10,"difficulty":"hard"},{"identifier":"ad61ab1–43223ef–bc24c7d–2583be6","challenge":"Stage 1: Search the letter \’O\’ west of Pleasent Park","total":1,"stars":2,"difficulty":"normal"}],"week5":[{"identifier":"d09bf41–544a336–5a46c90–77ebb5e","challenge":"Stage 1: Land at Polar Peak","total":1,"stars":1,"difficulty":"normal"},{"identifier":"d09bf41–544a336–5a46c90–77ebb5e","challenge":"Deal damage to opponent\’s structures","total":5000,"stars":5,"difficulty":"normal"},{"identifier":"d09bf41–544a336–5a46c90–77ebb5e","challenge":"Suppressed weapon eliminations","total":3,"stars":10,"difficulty":"hard"},{"identifier":"d09bf41–544a336–5a46c90–77ebb5e","challenge":"Stage 1: Dance on top of a Water Tower","total":1,"stars":1,"difficulty":"normal"},{"identifier":"d09bf41–544a336–5a46c90–77ebb5e","challenge":"Search Chests at Wailing Woods or Paradise Palms","total":7,"stars":5,"difficulty":"normal"},{"identifier":"d09bf41–544a336–5a46c90–77ebb5e","challenge":"Search between a Giant Rock Man, a Crowned Tomato and an Encircled Tree","total":1,"stars":10,"difficulty":"hard"},{"identifier":"d09bf41–544a336–5a46c90–77ebb5e","challenge":"Eliminate an opponent from closer than 5m away","total":3,"stars":10,"difficulty":"hard"}],"week6":[{"identifier":"fbd7939–d674997–cdb4692–d34de86","challenge":"Search an Ammo Box in different Named Locations","total":7,"stars":5,"difficulty":"normal"},{"identifier":"fbd7939–d674997–cdb4692–d34de86","challenge":"Search Chilly Gnomes","total":7,"stars":5,"difficulty":"normal"},{"identifier":"fbd7939–d674997–cdb4692–d34de86","challenge":"Eliminate opponents in Lucky Landing or Tilted Towers","total":3,"stars":10,"difficulty":"hard"},{"identifier":"fbd7939–d674997–cdb4692–d34de86","challenge":"Stage 1: Visit Polar Peak and Tilted Towers in a single match","total":2,"stars":1,"difficulty":"normal"},{"identifier":"fbd7939–d674997–cdb4692–d34de86","challenge":"Slide a Ice Puck over 150m in a single throw","total":1,"stars":5,"difficulty":"normal"},{"identifier":"fbd7939–d674997–cdb4692–d34de86","challenge":"Stage 1: Deal damage with SMGs to opponents","total":200,"stars":3,"difficulty":"normal"},{"identifier":"fbd7939–d674997–cdb4692–d34de86","challenge":"Deal damage with different weapons in a single match","total":5,"stars":10,"difficulty":"hard"}],"week7":[{"identifier":"28dd2c7–955ce92–6456240–b2ff010","challenge":"Visit all Expedition Outposts","total":7,"stars":5,"difficulty":"normal"},{"identifier":"28dd2c7–955ce92–6456240–b2ff010","challenge":"Use a Rift or Rift–To–Go in different matches","total":3,"stars":5,"difficulty":"normal"},{"identifier":"28dd2c7–955ce92–6456240–b2ff010","challenge":"Pistol Eliminations","total":3,"stars":10,"difficulty":"hard"},{"identifier":"28dd2c7–955ce92–6456240–b2ff010","challenge":"Stage 1: Land at Salty Springs","total":1,"stars":1,"difficulty":"normal"},{"identifier":"28dd2c7–955ce92–6456240–b2ff010","challenge":"Search Chests at Loot Lake or Frosty Flights","total":7,"stars":5,"difficulty":"normal"},{"identifier":"28dd2c7–955ce92–6456240–b2ff010","challenge":"Destroy flying X–4 Stormwings","total":1,"stars":10,"difficulty":"hard"},{"identifier":"28dd2c7–955ce92–6456240–b2ff010","challenge":"Stage 1: Damage opponents in a single match","total":200,"stars":3,"difficulty":"normal"}],"week8":[],"week9":[],"week10":[]}}"
If you’re going to code along with this example be sure to place
this at the top of the code.
The image below shows the JSON structure for the challenges
from the API.

Now to begin with we’ll create a decodable struct for the first
level of data and print that:
// using Codable for the first layer of data
struct FortniteChallenges: Codable {
// in this json we know that all the data exists, you can use
// optionals just in case
let language: String
let season: Int
let currentweek: Int
let star: String
}
// we now need to simply decode the data using JSON decoder and pass
// in the struct we created defining the structure of the JSON
if let jsonData = json.data(using: .utf8) {
let decoder = JSONDecoder()
// you might notice here i used try! for simplicity, this would crash your
// program if a JSON value defined does not exist
let challengesData = try! decoder.decode(FortniteChallenges.self, from: jsonData)
// we’ll print out the values here
print("Language: \(challengesData.language)")
print("Season: \(challengesData.season)")
print("Current Week: \(challengesData.currentweek)")
print("Star image url: \(challengesData.star)")
}
As you can see Swift does almost all the work for us as long
as the names and types match those within the JSON data.
One common case is that you’ll have data in the JSON that isn’t always present, while that doesn’t happen here Swift optionals, and we’ll include an extra literal in this example for this case:
let language: String
let season: Int
let currentweek: Int
let star: String
// if a key does not always exist you can use an optional
// so that JSONEncoder does not throw an error but instead
// sets the value to nil
let notreallyhere: String?
}
Now looking at the structure we see that the challenges value
contains a nested array with each week, and within that another array containing
the challenges for that specific week.
To handle this we’ll create a String dictionary for the week
name, and within that place the challenges. We’ll then print out the weeks and each challenge from that week.
as in the example:
let language: String
let season: Int
let currentweek: Int
let star: String
// we’ll create a challenges dictionary here for the
// week string within which we’ll store the data
// for each challenge
let challenges: [String: [Challenge]]
struct Challenge: Codable {
let identifier: String
let challenge: String
let total: Int
let stars: Int
let difficulty: Difficulty
enum Difficulty: String, Codable {
case normal = "normal"
case hard = "hard"
}
}
}
if let jsonData = json.data(using: .utf8) {
let decoder = JSONDecoder()
let challengesData = try! decoder.decode(FortniteChallenges.self, from: jsonData)
print("Language: \(challengesData.language)")
print("Season: \(challengesData.season)")
print("Current Week: \(challengesData.currentweek)")
print("Star image url: \(challengesData.star)")
// Iterate through each week of challenges within
// the data and print the week
for (week, challenges) in challengesData.challenges {
print("*******")
print("Week: \(week)")
// iterate through each challenge and print the
// data
for challenge in challenges {
print("Identifier: \(challenge.identifier)")
print("Challenge: \(challenge.challenge)")
print("Total: \(challenge.total)")
print("Stars: \(challenge.stars)")
print("Difficulty: \(challenge.difficulty)")
}
}
}
Now you might want further type safety, since we know that Difficulty
can only be normal or hard we’ll create an enum for those possibilities and example
we’ll also use a filter to display only the hard challenges:
let language: String
let season: Int
let currentweek: Int
let star: String
let challenges: [String: [Challenge]]
struct Challenge: Codable {
let identifier: String
let challenge: String
let total: Int
let stars: Int
// we’ll change the difficulty to an enum
// this will contain two cases for the
// two difficulty possibilities
let difficulty: Difficulty
enum Difficulty: String, Codable {
case normal = "normal"
case hard = "hard"
}
}
}
if let jsonData = json.data(using: .utf8) {
let decoder = JSONDecoder()
let challengesData = try! decoder.decode(FortniteChallenges.self, from: jsonData)
print("Language: \(challengesData.language)")
print("Season: \(challengesData.season)")
print("Current Week: \(challengesData.currentweek)")
print("Star image url: \(challengesData.star)")
// loop through the challenges
for (week, challenges) in challengesData.challenges {
print("*******")
print("Week: \(week)")
// we’ll filter out and show only hard difficulty
// challenges
for challenge in challenges where challenge.difficulty.rawValue == "hard" {
print("Identifier: \(challenge.identifier)")
print("Challenge: \(challenge.challenge)")
print("Total: \(challenge.total)")
print("Stars: \(challenge.stars)")
print("Difficulty: \(challenge.difficulty)")
}
}
}
Trying this code you might have noticed that the challenges
are unsorted, that’s because they are contained with a dictionary.
In this final example we’ll sort them by week, because the
week values contain both strings and numbers we’ll use the string function
localizedStandardCompare so everything sorts properly (otherwise week10 would appear before week2).
let language: String
let season: Int
let currentweek: Int
let star: String
let challenges: [String: [Challenge]]
struct Challenge: Codable {
let identifier: String
let challenge: String
let total: Int
let stars: Int
let difficulty: Difficulty
enum Difficulty: String, Codable {
case normal = "normal"
case hard = "hard"
}
}
}
if let jsonData = json.data(using: .utf8) {
let decoder = JSONDecoder()
let challengesData = try! decoder.decode(FortniteChallenges.self, from: jsonData)
print("Language: \(challengesData.language)")
print("Season: \(challengesData.season)")
print("Current Week: \(challengesData.currentweek)")
print("Star image url: \(challengesData.star)")
// we’ll sort the challenges dictionary here because
// the week value contains both text and numbers we’ll
// use localizedStandardCompare to make sure that they
// are sorted properly
let challenges = challengesData.challenges.sorted(by: {
$0.0.localizedStandardCompare($1.0) == .orderedAscending
})
// loop through the new sorted challenges
for (week, challenges) in challenges {
print("*******")
print("Week: \(week)")
for challenge in challenges where challenge.difficulty.rawValue == "hard" {
print("Identifier: \(challenge.identifier)")
print("Challenge: \(challenge.challenge)")
print("Total: \(challenge.total)")
print("Stars: \(challenge.stars)")
print("Difficulty: \(challenge.difficulty)")
}
}
}
Hopefully this has helped for anyone decoding JSON
and dealing with JSON that includes nested values.
Original article: JSON Decoding in Swift Example – Decoding Nested JSON From The Free Fortnite API
©2019 iOS App Dev Libraries, Controls, Tutorials, Examples and Tools. All Rights Reserved.