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

SOLVED: Hacking arend with JSON invalid key containing a space - no snakes or camels

Forums > Swift

I've been literally 'hacking around' with JSON and collecting data from external URL's, currently my code isn't failsafe as i'm stepping through to see if everyone who has published the JSON file is conforming to the same specification. What I've found is some oddities which I'm able to catch and code for. but there is one that I don't know how to handle. Where the publisher of the JSON file has included a key name with a space ;

example : instead of something like {"some_id":"123456"} instead they have published as {"some id":"123456"}

So the key is neither camelCase nor snake_case.

Ideally the publisher of the JSON should change their file so that it conforms, but is there a slick way that I could change how the Codable decode works to cater for this rogue space.

I am wondering if the best approach would be to 'collect' the JSON from external source, run it through some sort of validation function where I check purely for rogue string values in the keys and correct them, then safe the file locally and then use that file. to extract the JSON data from using codable structure in swift. But so far I've only found once instance of this rogue space within one key and thus wondered if there is a simpler way to catch and correct it so the JSON file decodes without issue.

3      

This is a good example of when you would use a CodingKeys enum.

Since you didn't supply a sample of your JSON, I had to just make something up, but this simple example should suffice to illustrate the point:

import Foundation

let jsonData = """
{
    "first name": "Charlotte",
    "last name": "Grote",
    "occupation": "Mystery Girl",
    "age": 19
}
""".data(using: .utf8)!

struct Person: Codable {
    let firstName: String
    let lastName: String
    let occupation: String
    let age: Int

    private enum CodingKeys: String, CodingKey {
        case firstName = "first name"
        case lastName = "last name"
        case occupation, age
    }
}

do {
    let person = try JSONDecoder().decode(Person.self, from: jsonData)
    print("\(person.firstName) \(person.lastName): \(person.occupation) (\(person.age))")
} catch {
    print(error)
}

The tricky bit is that if you specify one key in a CodingKeys enum, you have to specify them all. Kind of a pain, but the benefits outweigh the painfulness.

3      

Great reply @roosterboy, it's the tricky bit that i'm struggling with.

a. the structure is a nested structure - eg. in your example say Person has sub-structures for fullName containing firstName, middleName and lastName and that maybe age would also have date of birth and place of birth ;
b. Some JSON files would have "first name" and some "first_name"

for scenario a. I'm struggling to see how the enum should be declared to cater for the substructures too - I figured that they all need to be enumerated so that they are valid but I keep getting compile error saying that the structure isn't codable.

for scenario b. my line of thinking is that I could declar the Key as optional with ? and then also define it in the enum, something like, using your example firstName : String? first_name : String?

then when reading the different JSON files if it's got "first name" that can be defined in the enum but if it's got "first_name" that would decode fine and I can test firstName and first_name for nil to ensure I capture the populated one from the JSON.

3      

Please show an example of what you are talking about. It's too difficult to provide a good answer without seeing some code.

4      

I used your code example, modified it for the nested structure(s) that I'm having to work with and I think i've cracked it...it needs mulitple private enum's one for each region of the hierarchy.... as you said you need to enum them all, but when nested the enums are nested within the relevant section and can't be done in a single block.

import Foundation

let jsonData = """
{
    "businessName" : "My Business Name",
    "lineOfBusiness" : "Storytelling",

    "name" :
    {
    "first name": "Charlotte",
    "last name": "Grote",
     "recDetails" : {
            "fileReference": "AX10101P",
            "fileTitle" : "Long Term absence record"
                },
    "age": 19
    }
}
""".data(using: .utf8)!

struct Personnel: Codable {
    let businessName : String
    let lineOfBusiness : String

    struct name: Codable
    {

        let firstName: String
        let lastName: String
        let age: Int

        struct recDetails:  Codable
        {

            let fileReference : String
            let fileTitle : String

            private enum CodingKeys: String, CodingKey {

                 case fileReference
                case  fileTitle
             }
        } //end of recDetails - record associated with the person

        var recDetails : recDetails

        private enum CodingKeys: String, CodingKey {

             case firstName = "first name"
             case lastName = "last name"
             case age
             case recDetails
         }
    } // name structure

    var name : name

    private enum CodingKeys: String, CodingKey {
         case businessName, lineOfBusiness
         case name
     }
} // end of Personnel structure

do {
    let person = try JSONDecoder().decode(Personnel.self, from: jsonData)
    print("\(person.businessName) \(person.lineOfBusiness): \(person.name.firstName) \(person.name.lastName) ")
} catch {
    print(error)
}

Note : the JSON file here is single element, but in reality it would have [] within it for multiple array entries.

3      

Yep, I think you got the hang of it.

A few comments...

struct name
struct recDetails

var recDetails : recDetails
var name : name

The convention in Swift is for type names to start with a capital letter and variables to start with a lowercase. It aids readability and helps others (including Future!You) understand your code better. So these should be:

struct Name
struct RecDetails

var recDetails : RecDetails
var name : Name

With CodingKeys you only need to specify them if the key names in your JSON are different from the member names in your structs. If they are the same—or if you can handle it with convertFromSnakeCase—then you don't need to include a CodingKeys enum. So you don't really need the enums under your Personnel and RecDetails types since the key names match the member names.

When I said "The tricky bit is that if you specify one key in a CodingKeys enum, you have to specify them all." I was actually referring to the fact that if you use CodingKeys to indicate a different name for one key in a struct, you have to do all the keys in that struct. Like how in your Name struct you actually only need to rename "first name" and "last name" but due to the way CodingKeys works, you have to include cases for age and recDetails too. Make sense?

4      

@roosterboy - Thank you really useful. Appreciate your time and inputs.

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.