WWDC22 SALE: Save 50% on all my Swift books and bundles! >>

Day 60 - Milestone Challenge - JSONDecoder Question

Forums > 100 Days of SwiftUI

Hello, I'm slowly working through the milestone challenge for projects 10-12, and am having an issue with the JSONDecoder step of parsing the friendface.json file. I'm getting the following message when I try to decode the JSON contents:

error: keyNotFound(CodingKeys(stringValue: "users", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"users\", intValue: nil) (\"users\").", underlyingError: nil))

I have the following structs in my project:

struct Response: Codable {
    var users: [User]
}

struct User: Codable {
    var id : UUID
        var isActive: Bool
        var name: String
        var age: Int
        var company: String
        var email: String
        var address: String
        var about: String
        var registered: Date = Date()
        var tags: [String] = []
        var friends: [Friend] = []

    enum codingKeys: CodingKey {
        case id, isActive, name, age, company, email, address, about, registered, tags, friends
    }
}

My ContentView is pretty simple, and includes a single @State var:

struct ContentView: View {
    @State private var results = [User]()
    var body: some View {
        VStack {
            Text("List follows....")
            List(results, id: \.id) { item in
                Text("User id: \(item.id)")
            }
        }
        .task {
            await loadData()
        }
    }

    func loadData() async {
        guard let url = URL(string: "https://hackingwithswift.com/samples/friendface.json") else {
            print("Invalid URL")
            return
        }
        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            do {
                let decoder = JSONDecoder()
                print("Decoder created")
                let decodedResponse = try decoder.decode([Response].self, from: data)
                print("DecodedResponse created")
            }
            catch {
                print("error: ", error)
            }
        } catch {
            print("Invalid data")
            print(error)
        }
    }
}

I modeled this process on the material discussed in day 49, but clearly, something is different about the data. Looking at the ITunes data, it formats the data differently by including a resultCount header. The friendface.json file does not have that. I'm not clear on where/why the decoding process is looking for a "users" key, or how to tell the decoder to skip over that and process the array items. I've tried distilling the code down by commenting out all but the id variable in the User struct, but I get the same results.

Any thoughts on this would be sincerely appreciated. Thanks for taking the time to look at this!

   

Mitchell battles the JSON demons!

but clearly, something is different about the data. I'm not clear on where/why the decoding process is looking for a "users" key,

You gave yourself a clue! Follow the clue!

Where in your code do you mentions users?

// Found it!
struct Response: Codable {
    var users: [User]  // <--- Here it is.
}

Now, where in your code do you reference a Response object?

// Found a Response object!
 let decodedResponse = try decoder.decode([Response].self, from: data)

Now take a close look at the raw JSON data.
What you're looking at is an array of User objects from hackingwithswift.com/samples/friendface.json

But what you're trying to decode is an array of Response objects. The only contents of the Response object is a single variable named users. Swift compiler barfs here. You don't define users as a JSON key.

This is NOT what you're trying to decode!

This is important to understand. You are decoding ONE user object. You just happen to get a whole bunch of them back.

Think what you're trying to decode! The decodedResponse should be a bunch of User objects, not a bunch of Response objects!

Instead try:

    let decodedResponse = try decoder.decode([User].self, from: data)  // decode an array of User objects
    results = decodedResponse

Come back and share your results.

   

"Mitchell battles the JSON demons!" - That's about as accurate as it could be. With the suffix of "And he's losing!" would also be appropriate.

Your solution was exactly what I needed, and I appreciate the clarification. Making the code change to an array of Users being returned corrects the issue, and I'm able to move forward. I really appreciate the assistance.

During my initial troubleshooting, I had tried the following code:

// Found a Response object!
let decodedResponse = try decoder.decode(Response.self, from: data)

When I tried that line, with the Response struct defined exactly as above, the JSON Decoder returned an error of:

" error: typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil)) "

Since Response effectively consists of a variable containing an array of Users, I am puzzled why the Decoder thinks it is trying to decode a Dictionary. Can you shed any light on why this approach also fails? (Those JSON demons strike again!)

Once again, thanks for your generous assistance on this!

   

struct Response: Codable {
    var users: [User]
}

Because you have the Response set up to look for a key of users that contains an array of User objects. That's a Dictionary.

It's looking for this:

{
   "users": [
       {
           "id": "50a48fa3-2c0f-4397-ac50-64da464f9954",
           "isActive": false,
           ...
       },
       ...
   ]
}

But what it's finding is this:

[
    {
       "id": "50a48fa3-2c0f-4397-ac50-64da464f9954",
       "isActive": false,
       ...
    },
    ...
]

   

To add to @rooster's description.

You defined Response like a dictionary, like this:

// response has ONE entry in the dictionary named users.
// users may contain many user items.
Response
// The dictionary "term" users is defined as a collection of users.
users:     user
           user
           user
           user
           user
           user

But if you look closely at the JSON, it's just defined as a loose listing of users, something like this:

// The JSON is just a loose collection of user objects.
[
           user
           user
           user
           user
           user
           user
           user
]

The error stated: "Expected to decode Dictionary<String, Any> but found an array instead." So, the decoder had problems trying to map the loose collection of users, to a dictionary.

Extended JSON Example

This is not part of your solution, but might help clarify the "dictionary" concept. Think about some other JSON that you're examining and want to decode. The JSON might have a collection of users, but it might ALSO have a collection of flowerShops and a collection of taxi cabs. How do you find just the flowerShops in a dictionary? Easy. Like a hardback dictionary, flip to the section named "flowerShops".

// JSON that resembles a dictionary
[
users:     user
           user
           user
           user
           user
           user
shops:     flowerShop
           flowerShop
           flowerShop
           flowerShop
           flowerShop
           flowerShop
taxis:     cab
           cab
           cab
           cab
           cab
 ]

   

Thank you both for your explanations. I'm getting a little tripped up by dictionaries, as the difference between

struct Response: Codable {
   var users: [User]
 }

and

struct Offspring: Codable {
   var childrenNames: [String]
}

eludes me. I gather that the the first is somehow a dictionary, but the second is a struct that just contains an array of strings. I thought a dictionary was defined as

var myDictionary: [type, type]

which is quite different from the structs I've declared. (Insert sound of head scratching here...)

In any case, if I may indulge in one final question about my original problem:

I wondered about changing my original defintion of Response to that of an array of User objects, so I change the code to read:

var Response: [User] = []

then changed the decoder line to:

let decodedResponse = try decoder.decode(Response.self, from: data)

The results in a compiler error of:

Cannot convert value of type '[User]' to expected argument 'T.Type' Generic parameter 'T' could not be inferred

I'm not sure how to interpret that, as I thought that

decoder.decode([User].self, from: data)

would have been equivalent to

decoder.decode(Response.self, from: data)

as long as Response was a variable of type [User]

Thanks once again for your patience in this, as I learn the nuances of JSON decoding. I really do appreciate it!

   

When you say:

var Response: [User] = []

you are creating a property called Response that has a type of [User].

When you say:

decoder.decode(Response.self, from: data)

you tell the decoder that you want to decode something that has a type of Response.

These two lines:

decoder.decode([User].self, from: data)

decoder.decode(Response.self, from: data)

will only be equivalent if you also have this somewhere in your code:

typealias Response = [User]

which sets up Response as just another name for [User]

   

As for your confusion over dictionaries...

A dictionary is defined as a key plus a value. So, in Swift, for instance:

let dict: [String: Int] = [:]

defines an empty Dictionary with a String key and Int value.

But remember, you aren't just dealing with Swift here, but also with JSON. In JSON, a dictionary looks like this:

{
    "key": 1
}

(or whatever type the value is.)

When you use this data type in Swift to decode some JSON:

struct Response: Codable {
   var users: [User]
}

you are telling the decoder that Response (a struct in Swift) corresponds to a dictionary in JSON that has a key called users and a value that is an array of objects that correspond to the User type in Swift.

   

Think you making getting confused. Paul always says start with the data.

If you used Ducky Model Editor and put copy of JSON in this you will get this

struct User: Codable {
  struct Friend: Codable {
    let id: String
    let name: String
  }

  let id: String
  let isActive: Bool
  let name: String
  let age: Int
  let company: String
  let email: String
  let address: String
  let about: String
  let registered: Date
  let tags: [String]
  let friends: [Friend]
}

You can then add Identifiable and change the id to UUID (this actually will not effect it to much even if you do not).

Paul did an extension helper so if you create a file called Decode-URLSession.swift and put this extension in (PS you can also save this to a code snippet for easy use in other projects).

/// A URLSession extension that fetches data from a URL and decodes to some Decodable type.
/// Usage: let user = try await URLSession.shared.decode(UserData.self, from: someURL)
/// Note: this requires Swift 5.5.
extension URLSession {
    func decode<T: Decodable>(
        _ type: T.Type = T.self,
        from url: URL,
        keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys,
        dataDecodingStrategy: JSONDecoder.DataDecodingStrategy = .deferredToData,
        dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate
    ) async throws  -> T {
        let (data, _) = try await data(from: url)

        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = keyDecodingStrategy
        decoder.dataDecodingStrategy = dataDecodingStrategy
        decoder.dateDecodingStrategy = dateDecodingStrategy

        let decoded = try decoder.decode(T.self, from: data)
        return decoded
    }
}

Then in your ContentView (or where ever you doing the call) add

@State private var users = [User]()

then add this method (again you can also save this to a code snippet for easy use in other projects)

/// Call for get JSON data from URL
/// requires `@State private var name = [Decodable]()`
/// and `.task { await fetch() }`
func fetch() async {
    do  {
        let userURL = URL(string: "<#File Name#>")!
        async let userItems = try await URLSession.shared.decode(<#Decodable#>.self, from: userURL)
        <#name#> = try await userItems
    } catch {
        print("Failed to fetch data!")
    }
}

change the "placeholders" and added dateDecodingStrategy: .iso8601 to call as there is a Date

func fetch() async {
    do  {
        let userURL = URL(string: "https://www.hackingwithswift.com/samples/friendface.json")!
        // added `dateDecodingStrategy` as there is an ISO8601 date in JSON but not always required.
        async let userItems = try await URLSession.shared.decode([User].self, from: userURL, dateDecodingStrategy: .iso8601)
        users = try await userItems
    } catch {
        print("Failed to fetch data!")
    }
}

then add the .task { await fetch() } to your list so your content view looks like this

struct ContentView: View {
    @State private var users = [User]()

    var body: some View {
        List(users) { user in
            Text(user.name)
        }
        .task { await fetch() }
    }

    /// Call for get JSON data from URL
    /// requires `@State private var name = [Decodable]()`
    /// and `.task { await fetch() }`
    func fetch() async {
        do  {
            let userURL = URL(string: "https://www.hackingwithswift.com/samples/friendface.json")!
            // added `dateDecodingStrategy` as there is an ISO8601 date in JSON but not always required.
            async let userItems = try await URLSession.shared.decode([User].self, from: userURL, dateDecodingStrategy: .iso8601)
            users = try await userItems
        } catch {
            print("Failed to fetch data!")
        }
    }
}

   

Hacking with Swift is sponsored by Emerge

SPONSORED Optimize your app’s startup time, binary size, and overall performance using Emerge’s advanced app optimization and monitoring tools. Reliably measure app size, speed up your app's startup time with Emerge's Launch Booster, and much more. Emerge is actively used by many of the top mobile development teams in the world.

Find out more

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.