TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

SOLVED: Decoding JSON

Forums > Swift

Im having to decode some Jason on that has a formatting similar to what is shown below.

{
"id" : "1234567",
"name" : "Bad JSON",
"item1" : "red",
"item2" : "grean",
"item3" : "blue"
}

Ideally, I want the items to be an array so my Swift struct would look like the following.

struct JSONData: Decodable {
    let name: String
    let id: String
    let item: [String]
}

Is there a way to write a custom init(from decoder: Decoder) throws { } so that it could handled JSON, even if it had an arbitrary number of items.

3      

Here's one way you could do it:

import Foundation

let jsonData = """
{
"id" : "1234567",
"name" : "Bad JSON",
"item1" : "red",
"item2" : "grean",
"item3" : "blue"
}
""".data(using: .utf8)!

struct JSONData: Decodable {
    let name: String
    let id: String
    let item: [String]
      //I would suggest renaming this to items since it's an array

    //create a custom CodingKey struct so we can create our own keys dynamically
    struct CustomKey: CodingKey {
        //this is the only one we care about...
        var stringValue: String

        init?(stringValue: String) {
            self.stringValue = stringValue
        }

        //...but we have to include this to satisfy the protocol
        var intValue: Int?

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

    //to give us a nice error to return
    //this is rather simple but could be fancied up if you so desire
    struct InvalidKeyError: Error {
        let invalidKey: String
    }

    //and here's our workhorse init method
    init(from decoder: Decoder) throws {
        //get the container that holds all of our keys
        let container = try decoder.container(keyedBy: CustomKey.self)

        //pull out the static keys we know about
        self.id = try container.decode(String.self, forKey: CustomKey(stringValue: "id")!)
        self.name = try container.decode(String.self, forKey: CustomKey(stringValue: "name")!)

        //now loop through all the keys and grab any that match item#
        //but first we need a temporary array to store the values
        var _item: [String] = []
        for key in container.allKeys {
            switch key.stringValue {
            case let value where value.contains("item"):
                //add this item to our item array
                _item.append(value)
            case "id", "name":
                //we already have these, so skip them
                continue
            default:
                //anything else, we throw an error
                throw InvalidKeyError(invalidKey: key.stringValue)
            }
        }

        //now init our struct property using the temporary array
        self.item = _item
    }
}

//give it a shot!
do {
    let jsonStruct = try JSONDecoder().decode(JSONData.self, from: jsonData)
    print(jsonStruct) //JSONData(name: "Bad JSON", id: "1234567", item: ["item2", "item3", "item1"])
} catch {
    print(error)
}

4      

Amazing that was super helpful. I modified the code a little bit because it was currently returning a list of the item variable names, not their actual Contant i.e. it would return ["item1", "item2", "item3"] rather than ["red", "blue", "green"]. I also added the ability to handle null values. These changes can be seen here:

init(from decoder: Decoder) throws {
        //get the container that holds all of our keys
        let container = try decoder.container(keyedBy: CustomKey.self)

        //pull out the static keys we know about
        self.id = try container.decode(String.self, forKey: CustomKey(stringValue: "id")!)
        self.name = try container.decode(String.self, forKey: CustomKey(stringValue: "name")!)

        //now loop through all the keys and grab any that match item#
        //but first we need a temporary array to store the values
        //change #1 add the ability to hold nill values
        var _items: [String?] = [] 
        for key in container.allKeys {
            switch key.stringValue {
            case let value where value.contains("item"):
                //change #2 decode the value of the discover key
                let newValue = try? container.decode(String.self, forKey: CustomKey(stringValue: value)!)
                //change #3 add this newValue to our item array not value
                _items.append(newValue)
            case "id", "name":
                //we already have these, so skip them
                continue
            default:
                //anything else, we throw an error
                throw InvalidKeyError(invalidKey: key.stringValue)
            }
        }

        //now init our struct property using the temporary array
        //change #4 use a compactMap to strip the nill values out
        self.items = _items.compactMap { $0 }
    }

@roosterboy I'm curious why you're making the init() and methods optional? All of the force unwrapping makes me nervous and it seems to work without it.

3      

I modified the code a little bit because it was currently returning a list of the item variable names, not their actual Contant i.e. it would return ["item1", "item2", "item3"] rather than ["red", "blue", "green"]. I also added the ability to handle null values.

Oops, my bad. I see what you mean and I'm not sure why I didn't catch that but you did so it's all good.

@roosterboy I'm curious why you're making the init() and methods optional? All of the force unwrapping makes me nervous and it seems to work without it.

Because that's what the CodingKey protocol calls for. I knew that the JSON used in the example would only ever have String keys, so it was safe to force unwrap. If you don't control the JSON you will be using then that might not be such a good idea and you should leave the inits as returning an Optional and check at the call site whether the result is nil or not.

Which brings up something I wanted to mention but then forgot to do so: The absolute best way to solve the issue you had in the first post would be to restructure the JSON. Something like this would be a far, far better choice:

{
"id" : "1234567",
"name" : "Bad JSON",
"items" : ["red", "grean", "blue"]
}

But, depending on where the JSON you are decoding is coming from, this may or may not be possible.

3      

Wanted to put the entirety of the code in one location to help anyone who finds this thread in the future. After a short discussion this is what we came up with:

let jsonData = """
{
"id" : "1234567",
"name" : "Bad JSON",
"item1" : "red",
"item2" : "grean",
"item3" : null
}
""".data(using: .utf8)!

struct JSONData: Decodable {
    let name: String
    let id: String
    let items: [String]
      //I would suggest renaming this to items since it's an array

    //create a custom CodingKey struct so we can create our own keys dynamically
    struct CustomKey: CodingKey {
        //this is the only one we care about...
        var stringValue: String

        init?(stringValue: String) {
            self.stringValue = stringValue
        }

        //...but we have to include this to satisfy the protocol
        var intValue: Int?

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

    //to give us a nice error to return
    //this is rather simple but could be fancied up if you so desire
    struct InvalidKeyError: Error {
        let invalidKey: String
    }

    //and here's our workhorse init method
    init(from decoder: Decoder) throws {
        //get the container that holds all of our keys
        let container = try decoder.container(keyedBy: CustomKey.self)

        //pull out the static keys we know about
        self.id = try container.decode(String.self, forKey: CustomKey(stringValue: "id")!)
        self.name = try container.decode(String.self, forKey: CustomKey(stringValue: "name")!)

        //now loop through all the keys and grab any that match item#
        //but first we need a temporary array to store the values
        var _items: [String?] = []
        for key in container.allKeys {
            switch key.stringValue {
            case let value where value.contains("item"):
                //decode the value of the discover key
                let newValue = try? container.decode(String.self, forKey: CustomKey(stringValue: value)!)
                //add this item to our item array
                _items.append(newValue)
            case "id", "name":
                //we already have these, so skip them
                continue
            default:
                //anything else, we throw an error
                throw InvalidKeyError(invalidKey: key.stringValue)
            }
        }

        //now init our struct property using the temporary array
        self.items = _items.compactMap { $0 }
    }
}

//give it a shot!
do {
    let jsonStruct = try JSONDecoder().decode(JSONData.self, from: jsonData)
    print(jsonStruct) //JSONData(name: "Bad JSON", id: "1234567", items: ["grean", "red"])
} catch {
    print(error)
}

3      

You can avoid the need for the compactMap like so:

var _items: [String] = []
for key in container.allKeys {
    switch key.stringValue {
    case let value where value.contains("item"):
        //add this item to our item array
        if let newValue = try? container.decode(String.self, forKey: CustomKey(stringValue: value)!) {
            _items.append(newValue)
        }
    case "id", "name":
        //we already have these, so skip them
        continue
    default:
        //anything else, we throw an error
        throw InvalidKeyError(invalidKey: key.stringValue)
    }
}

//now init our struct property using the temporary array
self.items = _items

3      

Hacking with Swift is sponsored by Blaze.

SPONSORED Still waiting on your CI build? Speed it up ~3x with Blaze - change one line, pay less, keep your existing GitHub workflows. First 25 HWS readers to use code HACKING at checkout get 50% off the first year. Try it now for free!

Reserve your 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.