Simple Swift UI Json App - decoding issue

Viewed 47

I am trying to create a simple Swift UI app to download some json from the web and then display it on the screen.

I have 2 calls - a general lookup call for an array of items which are displayed in a View. I then have a detailed lookup, based upon the id chosen by the user in the first View which is then displayed in a second View.

I am able to access the parsed data via 2 @Published properties in my NetworkController class which I access from my Views, using: .onAppear to trigger the calls when the user arrives at each view.

My first call is working and I am able to display a list of items and then select them and read their id, which I then use for the second call.

The second call is causing me problems, though and I am getting an error decoding.

As there is quite a bit of code I’ve created a single Playground which includes some sample Json and the functions which I use, which is producing the error.

I am doing both calls using the same procedure - e.g. a number of function - which I have applied from another app which does a similar thing. My guess is that the error is because I have misunderstood some part of how these work. Because of this, I’ve commented what I think they are doing, to give an idea of what I am trying to achieve. Because the first call is working I am thinking that my understanding is partially correct, but I must be missing something.

The element that differs between the first data call and the second one is that the first one only requires a single struct to parse all the data whereas the second call uses a nested struct. I am thinking that I may need to adjust something to accomodate this - I am using the outermost struct - but I’m not clear exactly what I need to do.

If anyone has any suggestions I would be very grateful.

######################################################
# PLAYGROUND 
######################################################

import Cocoa


var itemDetailed: ItemCodable? = nil
var item: Item

// ################################################################
// EXAMPLE JSON DATA
// ################################################################
let data = """
{
  "item": {
    "general": {
      "id": 11,
      "name": "app_install",
      "enabled": true,
      "trigger": "CHECKIN",
      "trigger_checkin": true,
      "trigger_enrollment_complete": false,
      "trigger_login": false,
      "trigger_network_state_changed": false,
      "trigger_startup": false,
      "trigger_other": "",
      "frequency": "Ongoing",
      "retry_event": "none",
      "retry_attempts": -1,
      "notify_on_each_failed_retry": false,
      "location_user_only": false,
      "target_drive": "/",
      "offline": false,
      "category": {
        "id": 3,
        "name": "Apps"
      },
      "date_time_limitations": {
        "activation_date": "",
        "activation_date_epoch": 0,
        "activation_date_utc": "",
        "expiration_date": "",
        "expiration_date_epoch": 0,
        "expiration_date_utc": "",
        "no_execute_on": {},
        "no_execute_start": "",
        "no_execute_end": ""
      },
      "network_limitations": {
        "minimum_network_connection": "No Minimum",
        "any_ip_address": true,
        "network_segments": []
      },
      "override_default_settings": {
        "target_drive": "default",
        "distribution_point": "",
        "force_afp_smb": false,
        "sus": "default"
      },
      "network_requirements": "Any",
      "site": {
        "id": -1,
        "name": "None"
      }
    },
    "scope": {
      "all_computers": false,
      "computers": [],
      "computer_groups": [
        {
          "id": 1,
          "name": "All Managed Clients"
        }
      ],
      "buildings": [],
      "departments": [],
      "limit_to_users": {
        "user_groups": []
      },
      "limitations": {
        "users": [],
        "user_groups": [],
        "network_segments": [],
        "ibeacons": []
      },
      "exclusions": {
        "computers": [],
        "computer_groups": [
          {
            "id": 9,
            "name": "app_installed_testutil"
          }
        ],
        "buildings": [],
        "departments": [],
        "users": [],
        "user_groups": [],
        "network_segments": [],
        "ibeacons": []
      }
    },
    "self_service": {
      "use_for_self_service": false,
      "self_service_display_name": "",
      "install_button_text": "Install",
      "reinstall_button_text": "Reinstall",
      "self_service_description": "",
      "force_users_to_view_description": false,
      "self_service_icon": {},
      "feature_on_main_page": false,
      "self_service_categories": [],
      "notification": "Self Service",
      "notification_subject": "app_install",
      "notification_message": ""
    },
    "package_configuration": {
      "packages": [
        {
          "id": 3,
          "name": "testutil_2.0.5.psr",
          "action": "Install",
          "fut": false,
          "feu": false
        }
      ]
    },
    "scripts": [],
    "printers": [
      ""
    ],
    "dock_items": [],
    "account_maintenance": {
      "accounts": [],
      "directory_bindings": [],
      "management_account": {
        "action": "doNotChange"
      },
      "open_firmware_efi_password": {
        "of_mode": "none",
        "of_password_sha256": "xxxxxyyyyyyyyyzzzzzzzzaaaabbbbbbccccccc"
      }
    },
    "reboot": {
      "message": "This computer will restart in 5 minutes.",
      "startup_disk": "Current Startup Disk",
      "specify_startup": "",
      "no_user_logged_in": "Restart if a package or update requires it",
      "user_logged_in": "Restart if a package or update requires it",
      "minutes_until_reboot": 5,
      "start_reboot_timer_immediately": false,
      "file_vault_2_reboot": false
    },
    "maintenance": {
      "recon": true,
      "reset_name": false,
      "install_all_cached_packages": false,
      "heal": false,
      "prebindings": false,
      "permissions": false,
      "byhost": false,
      "system_cache": false,
      "user_cache": false,
      "verify": false
    },
    "files_processes": {
      "search_by_path": "",
      "delete_file": false,
      "locate_file": "",
      "update_locate_database": false,
      "spotlight_search": "",
      "search_for_process": "",
      "kill_process": false,
      "run_command": ""
    },
    "user_interaction": {
      "message_start": "",
      "allow_users_to_defer": false,
      "allow_deferral_until_utc": "",
      "allow_deferral_minutes": 0,
      "message_finish": ""
    },
    "disk_encryption": {
      "action": "none"
    }
  }
}
""".data(using: .utf8)

// ################################################################
// DATA STRUCTS
// ################################################################

struct ItemCodable: Codable{
    let item: Item }

struct Item: Codable, Hashable, Identifiable {
    var id = UUID()
    
    let general: General?
    
    enum CodingKeys: String, CodingKey {
        case general = "general"
    }
}


struct General: Codable, Hashable, Identifiable {
    var id = UUID()
    let name: String?
    let enabled: Bool?
    let trigger: String?
    let triggerCheckin, triggerEnrollmentComplete, triggerLogin, triggerLogout: Bool?
    let triggerNetworkStateChanged, triggerStartup: Bool?
    let triggerOther, frequency: String?
    let locationUserOnly: Bool?
    let targetDrive: String?
    let offline: Bool?
    let networkRequirements: String?
    let mac_address: String?
    let ip_address: String?
    let payloads: String?
    
    enum CodingKeys: String, CodingKey {
        case name = "name"
        case enabled = "enabled"
        case trigger = "trigger"
        case triggerCheckin = "trigger_checkin"
        case triggerEnrollmentComplete = "trigger_enrollment_complete"
        case triggerLogin = "trigger_login"
        case triggerLogout = "trigger_logout"
        case triggerNetworkStateChanged = "trigger_network_state_changed"
        case triggerStartup = "trigger_startup"
        case triggerOther = "trigger_other"
        case frequency = "frequency"
        case locationUserOnly = "location_user_only"
        case targetDrive = "target_drive"
        case offline = "offline"
        case networkRequirements = "network_requirements"
        case mac_address = "mac_address"
        case ip_address = "ip_address"
        case payloads = "payloads"
    }
}


// ################################################################
// DECODE DATA
// ################################################################


struct ItemsDetailReply: Codable {
//    Struct to parse data
    let itemDetailed: ItemCodable
    static func decode(_ data: Data) -> Result<ItemCodable,Error> {
        let decoder = JSONDecoder()
        do {
            let response = try decoder.decode(ItemsDetailReply.self, from: data)
            print("ItemsDetailReply Decoding succeeded")
            separationLine()
            print("Response is:\n\(response)")
            return .success(response.itemDetailed)
        } catch {
            separationLine()
            print("Decoding error")
            return .failure(error)
        }
    }
}


func separationLine() {
    print("------------------------------------------------------------------")
}


// ################################################################
// DECODE AND REDIRECT TO MAIN QUEUE
// ################################################################


func processDetail(data: Data) {
// func that initiates the decoding using the decoding struct - then redirects the
// returned data to the main queue
    let decoded = ItemsDetailReply.decode(data)

    switch decoded {
    case .success(let itemDetailed):
        receivedItemDetail(itemDetailed: itemDetailed)
        separationLine()
        print("itemDetailed name is:\(String(describing: itemDetailed.item.general?.name))")
        separationLine()
    case .failure(let error):
        separationLine()
        print("Error encountered")
        separationLine()
        print(error)
        separationLine()
    }
}

// ################################################################
// SET PROPERTY VIA MAIN QUEUE
// ################################################################


func receivedItemDetail(itemDetailed: ItemCodable) {
    DispatchQueue.main.async {
//                self.itemDetailed = itemDetailed
    }
}

// CALL FUNCTION
processDetail(data: data!)
1 Answers

The problem lies in the decode method:

struct ItemsDetailReply: Codable {
//    Struct to parse data
    let itemDetailed: ItemCodable
    static func decode(_ data: Data) -> Result<ItemCodable,Error> {
        let decoder = JSONDecoder()
        do {
            let response = try decoder.decode(ItemsDetailReply.self, from: data)
            print("ItemsDetailReply Decoding succeeded")
            separationLine()
            print("Response is:\n\(response)")
            return .success(response.itemDetailed)
        } catch {
            separationLine()
            print("Decoding error")
            return .failure(error)
        }
    }
}

You're trying to decode a ItemsDetailReply but the data you pass into this method represents an ItemCodable. The decoder tries to locate a top level key itemDetailed but it fails.

You can probably ditch the ItemsDetailReply struct and just decode ItemCodable like this:

let response = try decoder.decode(ItemCodable.self, from: data)
Related