GO FURTHER, FASTER: Try the Swift Career Accelerator today! >>

Nested JSON - I am lost

Forums > SwiftUI

Worked with flat JSON for a while and had no issues to get stuff done! But now with a nested and for myself, very complex JSON I am a bit lost. Please see the code attached. I can load the JSON from a URL and get some data from the first layer e.g. code and status. But when trying to get more deeper into the JSON I am lost... Would like to get data from "data and "resuts", "splits"...

This is my early code, asking for some help: cheers chriskapeller


// Origional JSON from Triathlon.org API - this url includes reduced content
// https://trilysis.worknode.com/jsontest.json

import SwiftUI

class TriathlonResultsViewModel: ObservableObject {
    private struct ReturnedData: Codable {
        var code: Int
        var status: String
        // How to get values from data and results?
        // var data: [Data]
        // var results: [Result]
    }

    struct Data: Codable, Hashable {
        var prog_id: Int
        var event_id: Int
        var prog_name: String
    }

    struct Result: Codable, Hashable {
        var athlete_id: Int
        var athlete_title: String
        // how to get this array?
        // var splits: []
    }

    @Published var urlString = "https://trilysis.worknode.com/jsontest.json"

    func getData() async {
        // Create URL
        guard let url = URL(string: urlString) else {
            print("ERROR: Could not create a URL from \(urlString)")
            return
        }

        do {
            // Getting JSON in
            let (data, _) = try await URLSession.shared.data(from: url)

            // Decoding JSON
            guard let returnedData = try? JSONDecoder().decode(ReturnedData.self, from: data) else {
                print("JSON ERROR: Could not decode returned JSON data")
                return
            }

            // This is for local JSON data only
            // let returnedData = try JSONDecoder().decode(ReturnedData.self, from: jsonData)

            // print some values
            print("Code: \(returnedData.code)")
            print("Status: \(returnedData.status)")

        } catch {
            debugPrint(error)
            // print("Error decoding JSON: \(error.localizedDescription)")
        }
    }
}

struct JsonTest: View {
    @StateObject var triResults = TriathlonResultsViewModel()

    var body: some View {
        Button("Fetch Data") {
            Task {
                await triResults.getData()
            }
        }
    }
}

#Preview {
    JsonTest()
}

   

@chriska had an easy swim, but got lost during the next phase of the race....

But when trying to get more deeper into the JSON I am lost.....

Mapping Skills

Step back to the starting line and consider the basic building structures in Swift and other programming languages. A Swift struct can list simple data elements describing your object.

// A pizza order might collect simple data for a pizza order.
struct PizzaOrder {
    var size: String  // small, medium, large
    var cost: Double  // how many pounds, euros, or bars of gold-pressed latinum
} 

A more complex object may have a few data elements with a twist. One of the data elements might be made from one or several other objects.

struct PizzaOrder {
    var size:       String    // small, medium, large
    var pizzaUnit:  String    // slice, whole pie, etc
    var cost:       Double    // how many pounds, euros, or bars of gold-pressed latinum
    //                👇🏼 Toppings may be empty, or have many elements!
    var toppings:  [String]   // Pineapple, sausage, prawns, bacon, etc.
}

So the first step is to define many basic elements of your triathalon data feed.

I see your data contains events ( Herefordshire April Cold Plunge, etc). It also has athletes. Looking more deeply, athletes have split times for each leg. But consider the case where an athlete finishes two, but not three legs.

Design Goals

Your goal is to define structures in Swift that contain the individual fields, as well as the complex multi-container fields that map to your remote JSON source.

This is actually a valuable skill for Swift developers as many of your assignments will be exactly this. Pull data from a bank. Pull data from a weather station. Pull data from a spacecraft. Pull data from a surgeon's operating room in real time. You'll be given tasks to pull this data into your application then map this data to your internal structures.

Online Assistance

Try it on your own first! You need this skill, so attempt to map this on your own before looking online. Then you can compare your base knowledge to a solved solution. You need to address the gap. (Mind the gap!) Learn from this assignment.

Because this is such a common task in a developer's routine, you can bet that some JSON boffins created a tool to assist you. Visit app.quicktype.io for example.

This site allows you to paste in your triathalon JSON, and it will

  1. Evaluate your JSON
  2. Extract data types and subtypes.
  3. Stub out the structures in Swift.

A cool feature of this site is that it can stub out structures in several languages (Python, Java, C++, etc).

It won't always provide the best names for the structures, but you can take the stubbed code and improve it.

Keep Coding

Please return here and share your learning path.

1      

Thanks for the good advice and for the motivation!

Here's what I did:

  • Used the app.quicktype.io to get the structs
  • Played a bit arrond in Playgrounds
  • Two let with type DATE did not worke? I changed them to a STRING.
  • Made some easy print() functions to get some output on the console...

And it worked! I understand all ot the struct stuff, the CodingKeys and the JSONDecoder but I have little idea what all the classes and functions (below the JSON data) are doing? Any insight for me?

See my code

import Foundation
import SwiftUI

// This file was generated from JSON Schema using quicktype, do not modify it directly.
// To parse the JSON, add this file to your project and do:
//
//   let welcome = try? JSONDecoder().decode(Welcome.self, from: jsonData)

let jsonData = """

**(( Had to delete the JSON data here because it was to much text for the forum - please copy data into here))**

""".data(using: .utf8)!

// This file was generated from JSON Schema using quicktype, do not modify it directly.
// To parse the JSON, add this file to your project and do:
//
//   let welcome = try? JSONDecoder().decode(Welcome.self, from: jsonData)

import Foundation

// MARK: - Welcome
struct Welcome: Codable {
    let code: Int
    let status: String
    let data: DataClass
}

// MARK: - DataClass
struct DataClass: Codable {
    let progID, eventID: Int
    let progName: String
    let isRace: Bool
    let progDate, progDateUTC: String
    let progTime, progTimeUTC: JSONNull?
    let progTimezoneName, progTimezoneOffset, progGender: String
    let progMinAge, progMaxAge: Int
    let progDistanceCategory: String
    let progDistances: [JSONAny]
    let progNotes: JSONNull?
    let results: [Result]
    let team, liveTimingEnabled: Bool
    let event: Event
    let meta: Meta
    let headers: [Header]
    let headersCount: Int

    enum CodingKeys: String, CodingKey {
        case progID = "prog_id"
        case eventID = "event_id"
        case progName = "prog_name"
        case isRace = "is_race"
        case progDate = "prog_date"
        case progDateUTC = "prog_date_utc"
        case progTime = "prog_time"
        case progTimeUTC = "prog_time_utc"
        case progTimezoneName = "prog_timezone_name"
        case progTimezoneOffset = "prog_timezone_offset"
        case progGender = "prog_gender"
        case progMinAge = "prog_min_age"
        case progMaxAge = "prog_max_age"
        case progDistanceCategory = "prog_distance_category"
        case progDistances = "prog_distances"
        case progNotes = "prog_notes"
        case results, team
        case liveTimingEnabled = "live_timing_enabled"
        case event, meta, headers
        case headersCount = "headers_count"
    }
}

// MARK: - Event
struct Event: Codable {
    let eventID: Int
    let eventTitle, eventSlug: String
    //let eventEditDate: Date
    let eventEditDate: String
    let eventVenue, eventCountry: String
    let eventLatitude, eventLongitude: Double
    let eventDate, eventFinishDate, eventCountryIsoa2, eventCountryNOC: String
    let eventRegionID, eventCountryID: Int
    let eventRegionName, eventStatus: String
    let triathlonlive: Bool
    let eventCategories, eventSpecifications: [EventCategoryElement]
    let eventFlag: String
    let eventListing, eventAPIListing: String

    enum CodingKeys: String, CodingKey {
        case eventID = "event_id"
        case eventTitle = "event_title"
        case eventSlug = "event_slug"
        case eventEditDate = "event_edit_date"
        case eventVenue = "event_venue"
        case eventCountry = "event_country"
        case eventLatitude = "event_latitude"
        case eventLongitude = "event_longitude"
        case eventDate = "event_date"
        case eventFinishDate = "event_finish_date"
        case eventCountryIsoa2 = "event_country_isoa2"
        case eventCountryNOC = "event_country_noc"
        case eventRegionID = "event_region_id"
        case eventCountryID = "event_country_id"
        case eventRegionName = "event_region_name"
        case eventStatus = "event_status"
        case triathlonlive
        case eventCategories = "event_categories"
        case eventSpecifications = "event_specifications"
        case eventFlag = "event_flag"
        case eventListing = "event_listing"
        case eventAPIListing = "event_api_listing"
    }
}

// MARK: - EventCategoryElement
struct EventCategoryElement: Codable {
    let catName: String
    let catID: Int
    let catParentID: Int?

    enum CodingKeys: String, CodingKey {
        case catName = "cat_name"
        case catID = "cat_id"
        case catParentID = "cat_parent_id"
    }
}

// MARK: - Header
struct Header: Codable {
    let segment, name: String
    let units: String?
}

// MARK: - Meta
struct Meta: Codable {
    let temperatureWater, temperatureAir, humidity, wbgt: JSONNull?
    let wind, weather, wetsuit, headReferee: JSONNull?
    let competitionJury: String

    enum CodingKeys: String, CodingKey {
        case temperatureWater = "temperature_water"
        case temperatureAir = "temperature_air"
        case humidity, wbgt, wind, weather, wetsuit
        case headReferee = "head_referee"
        case competitionJury = "competition_jury"
    }
}

// MARK: - Result
struct Result: Codable {
    let athleteID: Int
    let athleteTitle, athleteFirst, athleteLast, athleteGender: String
    let validated: Bool
    let neutralStatus: Int
    let updatedAt: String
    let athleteProfileImage: JSONNull?
    let dob: String
    let athleteCountryID: Int
    //let athleteEditDate: Date
    let athleteEditDate: String
    let athleteYob: Int
    let athleteSlug, athleteNOC, athleteCountryName, athleteCountryIsoa2: String
    let athleteAge: Int
    let athleteListing: String
    let athleteFlag: String
    let athleteAPIListing: String
    let athleteCategories: [Int]
    let splits: [String]
    let resultID: Int
    let position: Position
    let totalTime: String
    let startNum: Int

    enum CodingKeys: String, CodingKey {
        case athleteID = "athlete_id"
        case athleteTitle = "athlete_title"
        case athleteFirst = "athlete_first"
        case athleteLast = "athlete_last"
        case athleteGender = "athlete_gender"
        case validated
        case neutralStatus = "neutral_status"
        case updatedAt = "updated_at"
        case athleteProfileImage = "athlete_profile_image"
        case dob
        case athleteCountryID = "athlete_country_id"
        case athleteEditDate = "athlete_edit_date"
        case athleteYob = "athlete_yob"
        case athleteSlug = "athlete_slug"
        case athleteNOC = "athlete_noc"
        case athleteCountryName = "athlete_country_name"
        case athleteCountryIsoa2 = "athlete_country_isoa2"
        case athleteAge = "athlete_age"
        case athleteListing = "athlete_listing"
        case athleteFlag = "athlete_flag"
        case athleteAPIListing = "athlete_api_listing"
        case athleteCategories = "athlete_categories"
        case splits
        case resultID = "result_id"
        case position
        case totalTime = "total_time"
        case startNum = "start_num"
    }
}

enum Position: Codable {
    case integer(Int)
    case string(String)

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let x = try? container.decode(Int.self) {
            self = .integer(x)
            return
        }
        if let x = try? container.decode(String.self) {
            self = .string(x)
            return
        }
        throw DecodingError.typeMismatch(Position.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for Position"))
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .integer(let x):
            try container.encode(x)
        case .string(let x):
            try container.encode(x)
        }
    }
}

// MARK: - Encode/decode helpers

class JSONNull: Codable, Hashable {

    public static func == (lhs: JSONNull, rhs: JSONNull) -> Bool {
            return true
    }

    public var hashValue: Int {
            return 0
    }

    public func hash(into hasher: inout Hasher) {
            // No-op
    }

    public init() {}

    public required init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            if !container.decodeNil() {
                    throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull"))
            }
    }

    public func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
            try container.encodeNil()
    }
}

class JSONCodingKey: CodingKey {
    let key: String

    required init?(intValue: Int) {
            return nil
    }

    required init?(stringValue: String) {
            key = stringValue
    }

    var intValue: Int? {
            return nil
    }

    var stringValue: String {
            return key
    }
}

class JSONAny: Codable {

    let value: Any

    static func decodingError(forCodingPath codingPath: [CodingKey]) -> DecodingError {
            let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode JSONAny")
            return DecodingError.typeMismatch(JSONAny.self, context)
    }

    static func encodingError(forValue value: Any, codingPath: [CodingKey]) -> EncodingError {
            let context = EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode JSONAny")
            return EncodingError.invalidValue(value, context)
    }

    static func decode(from container: SingleValueDecodingContainer) throws -> Any {
            if let value = try? container.decode(Bool.self) {
                    return value
            }
            if let value = try? container.decode(Int64.self) {
                    return value
            }
            if let value = try? container.decode(Double.self) {
                    return value
            }
            if let value = try? container.decode(String.self) {
                    return value
            }
            if container.decodeNil() {
                    return JSONNull()
            }
            throw decodingError(forCodingPath: container.codingPath)
    }

    static func decode(from container: inout UnkeyedDecodingContainer) throws -> Any {
            if let value = try? container.decode(Bool.self) {
                    return value
            }
            if let value = try? container.decode(Int64.self) {
                    return value
            }
            if let value = try? container.decode(Double.self) {
                    return value
            }
            if let value = try? container.decode(String.self) {
                    return value
            }
            if let value = try? container.decodeNil() {
                    if value {
                            return JSONNull()
                    }
            }
            if var container = try? container.nestedUnkeyedContainer() {
                    return try decodeArray(from: &container)
            }
            if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self) {
                    return try decodeDictionary(from: &container)
            }
            throw decodingError(forCodingPath: container.codingPath)
    }

    static func decode(from container: inout KeyedDecodingContainer<JSONCodingKey>, forKey key: JSONCodingKey) throws -> Any {
            if let value = try? container.decode(Bool.self, forKey: key) {
                    return value
            }
            if let value = try? container.decode(Int64.self, forKey: key) {
                    return value
            }
            if let value = try? container.decode(Double.self, forKey: key) {
                    return value
            }
            if let value = try? container.decode(String.self, forKey: key) {
                    return value
            }
            if let value = try? container.decodeNil(forKey: key) {
                    if value {
                            return JSONNull()
                    }
            }
            if var container = try? container.nestedUnkeyedContainer(forKey: key) {
                    return try decodeArray(from: &container)
            }
            if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) {
                    return try decodeDictionary(from: &container)
            }
            throw decodingError(forCodingPath: container.codingPath)
    }

    static func decodeArray(from container: inout UnkeyedDecodingContainer) throws -> [Any] {
            var arr: [Any] = []
            while !container.isAtEnd {
                    let value = try decode(from: &container)
                    arr.append(value)
            }
            return arr
    }

    static func decodeDictionary(from container: inout KeyedDecodingContainer<JSONCodingKey>) throws -> [String: Any] {
            var dict = [String: Any]()
            for key in container.allKeys {
                    let value = try decode(from: &container, forKey: key)
                    dict[key.stringValue] = value
            }
            return dict
    }

    static func encode(to container: inout UnkeyedEncodingContainer, array: [Any]) throws {
            for value in array {
                    if let value = value as? Bool {
                            try container.encode(value)
                    } else if let value = value as? Int64 {
                            try container.encode(value)
                    } else if let value = value as? Double {
                            try container.encode(value)
                    } else if let value = value as? String {
                            try container.encode(value)
                    } else if value is JSONNull {
                            try container.encodeNil()
                    } else if let value = value as? [Any] {
                            var container = container.nestedUnkeyedContainer()
                            try encode(to: &container, array: value)
                    } else if let value = value as? [String: Any] {
                            var container = container.nestedContainer(keyedBy: JSONCodingKey.self)
                            try encode(to: &container, dictionary: value)
                    } else {
                            throw encodingError(forValue: value, codingPath: container.codingPath)
                    }
            }
    }

    static func encode(to container: inout KeyedEncodingContainer<JSONCodingKey>, dictionary: [String: Any]) throws {
            for (key, value) in dictionary {
                    let key = JSONCodingKey(stringValue: key)!
                    if let value = value as? Bool {
                            try container.encode(value, forKey: key)
                    } else if let value = value as? Int64 {
                            try container.encode(value, forKey: key)
                    } else if let value = value as? Double {
                            try container.encode(value, forKey: key)
                    } else if let value = value as? String {
                            try container.encode(value, forKey: key)
                    } else if value is JSONNull {
                            try container.encodeNil(forKey: key)
                    } else if let value = value as? [Any] {
                            var container = container.nestedUnkeyedContainer(forKey: key)
                            try encode(to: &container, array: value)
                    } else if let value = value as? [String: Any] {
                            var container = container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key)
                            try encode(to: &container, dictionary: value)
                    } else {
                            throw encodingError(forValue: value, codingPath: container.codingPath)
                    }
            }
    }

    static func encode(to container: inout SingleValueEncodingContainer, value: Any) throws {
            if let value = value as? Bool {
                    try container.encode(value)
            } else if let value = value as? Int64 {
                    try container.encode(value)
            } else if let value = value as? Double {
                    try container.encode(value)
            } else if let value = value as? String {
                    try container.encode(value)
            } else if value is JSONNull {
                    try container.encodeNil()
            } else {
                    throw encodingError(forValue: value, codingPath: container.codingPath)
            }
    }

    public required init(from decoder: Decoder) throws {
            if var arrayContainer = try? decoder.unkeyedContainer() {
                    self.value = try JSONAny.decodeArray(from: &arrayContainer)
            } else if var container = try? decoder.container(keyedBy: JSONCodingKey.self) {
                    self.value = try JSONAny.decodeDictionary(from: &container)
            } else {
                    let container = try decoder.singleValueContainer()
                    self.value = try JSONAny.decode(from: container)
            }
    }

    public func encode(to encoder: Encoder) throws {
            if let arr = self.value as? [Any] {
                    var container = encoder.unkeyedContainer()
                    try JSONAny.encode(to: &container, array: arr)
            } else if let dict = self.value as? [String: Any] {
                    var container = encoder.container(keyedBy: JSONCodingKey.self)
                    try JSONAny.encode(to: &container, dictionary: dict)
            } else {
                    var container = encoder.singleValueContainer()
                    try JSONAny.encode(to: &container, value: self.value)
            }
    }
}

let welcome = try JSONDecoder().decode(Welcome.self, from: jsonData)
print("Code: \(welcome.code)")
print("Prog ID: \(welcome.data.progID)")
print("Event Title: \(welcome.data.event.eventTitle)")
print("Athlet Title: \(welcome.data.results[0].athleteTitle)")
print("Split 0: \(welcome.data.results[0].splits[0])")

//Dump all of it
print("\nDump all:")
dump(welcome)

One question - I dont need all the encodeing stuff - right? Since I am only decoding the JSON data?

So now I will try to get a working prototype in xCode with some usefull output in a list or table view...

Thx

   

Take a moment and read the responses to a decoding question in the HackingWithSwift forums. There are two approaches for decoding a string date from the JSON into a usable Swift Date type.

See -> Morphing JSON strings into Dates

Keep Coding

Did this help?

   

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's all new Paywall Editor allow you to remotely configure your paywall view without any code changes or app updates.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.