UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

For programmers who would like to organize their JSON text for easier viewing

Forums > SwiftUI

The database application that stores all the data I'm going to be using for my app can basically only export strings. So even if I have tables of data, they get written in CSV as "text;text;text" etc. (The database also spits out a ton of categories I don't want, and it's easier to delete them by deleting columns of a spreadsheet than using a texteditor; thus, the CSV export.) Either way, all the fields/properties get mixed around as well; for example:

[{"stateFlagImage":"okFlag.png", "name":"Oklahoma", "cityPopulations":"130;700;410", "yearOfStatehood":"1907", 
"cities":"Norman;Oklahoma City;Tulsa"}, {"stateFlagImage":"alFlag.png", "name":"Alabama", "cityPopulations":"200;220;200", 
"yearOfStatehood":"1819", "cities":"Birmingham;Huntsville;Montgomery"}, {"stateFlagImage":"miFlag.png", "name":"Michigan", 
"cityPopulations":"620;200;140", "yearOfStatehood":"1837", "cities":"Detroit;Grand Rapids;Warren City"}]

My preference would be for that to read:

[    {"name": "Oklahoma",
        "yearOfStatehood": "1907",
        "cities": [  {
                "name": "Norman",
                "population": "130000",
                "isCapital": "false"},
            {"name": "Oklahoma City",
                "population": "700000",
                "isCapital": "false"},
            {"name": "Tulsa",
                "population": "410000",
                "isCapital": "false"}  ]
        "stateFlagImage": "okFlag.png"},
      {"name": "Alabama",
        "yearOfStatehood": "1819",
        ...

In this example, I also wanted to add a Bool to the city arrays, which I set automatically to false, and then I could replace the Bool value for the one capital in each state with true if need be. (I would more likely add the field to my database and export it the next time I wanted to go through the process of CSV -> JSON.

Here's the code I used, including the fixes I made to the incoming data to remove the ";"s and convert the population strings (originally in thousands-of-people units) into the correct numerical values.

Here's the struct for the incoming JSON'd data:

struct OldStructure: Codable {
    let name: String

    let cities: String
    var cityArray: Array<String> {
        cities.components(separatedBy: ";")
    }

    // I couldn't figure out how to get the following two processes in the same closure, so I turned the String into an Array of Strings in the first and then turned each String in that Array into an Int in the second
    let cityPopulations: String
    var cityPopulationArray: Array<String> {
        cityPopulations.components(separatedBy: ";")
    }
    var cityPopsAsInts: Array<Int> {
        cityPopulationArray.map( { popString in
            (Int(popString) ?? 0) * 1000
        })
    }

    let yearOfStatehood: String
    let stateFlagImage: String
}

With the following being what you might want the actual struct in your app to look like:

struct NewStructure: Codable {

    let name: String                                   // property   (for purposes of my comments in rewriteJSON() code below)
    let yearOfStatehood: Int                           // property
    let stateFlagImage: String                          // property

    let cities: Array<City>                                // property

    struct City: Codable {
        let name: String                              // subproperty
        let population: Int                           // subproperty
        let capital: Bool                              // subproperty
    }
}

So here's the restructuring template (you'll need to adjust the commented values to your specifications

func rewriteJSON(oldArrangement: [OldStructure]) {
    var properties: [String] = Array(repeating: "", count: 4)      // number of properties in new arrangement (name, yearOfStatehood, cities, and stateFlagImage in this example)
    var propertiesObjects: [String] = []
    var finalForm: String = ""

    for state in oldArrangement {
        properties[0] = "\"name\": \"\(state.name)\""
        properties[1] = "\"yearOfStatehood\": \(state.yearOfStatehood)"

        let subset = { () -> String in      // building array for properties[2]
            var subproperties: [String] = Array(repeating: "", count: 3)        // count = number of subproperties in new arrangement (name, population, and isCapital in this example)
            var subpropertiesObjects: [String] = Array(repeating: "", count: state.cityArray.count)

            for i in 0..<state.cityArray.count {
                subproperties[0] = "\"name\":\"\(state.cityArray[i])\""
                subproperties[1] = "\"population\": \(state.cityPopsAsInts[i])"
                subproperties[2] = "\"isCapital\": false"
                subpropertiesObjects[i] = "{ \(subproperties.joined(separator: ", ")) }"
            }

            return "[ \(subpropertiesObjects.joined(separator: ", ")) ]"
        }

        properties[2] = "\"cities\": \(subset())"
        properties[3] = "\"stateFlagImage\": \"\(state.stateFlagImage)\""

        propertiesObjects.append("{ \(properties.joined(separator: ", ")) }")

    }
    finalForm = "[ \(propertiesObjects.joined(separator: ", ")) ]"

    // If you just want to copy and paste from the console, use the following print() command:
    print(finalForm)

    // If you want to save the JSON to a new .txt file, use the following do/catch command:
    // ** Note: this only works in Xcode, as far as I could tell.
    do {
        let dataToFile = Data(finalForm.utf8)

        let user = "~userName~"       // enter your Mac username
        let folder = "~Desktop~"      // enter the name of the folder in which to store the file (i.e., Desktop, Documents, Pictures, etc.) if you don't just want it to land directly inside your Home folder
        let fileName = "~restructuredJSON~"       // give the file a name
        let fileURL = URL(filePath: "/Users/\(user)/\(folder)/\(fileName).txt")

        try dataToFile.write(to: fileURL)
    } catch {
        print("Error")
    }
}

Here's the simple interface I used to "activate" the rewrite (statedata.json is the name of the file with the original, unsorted JSON from the first code block above):

struct ContentView: View {
    var body: some View {
        Button("Press to convert", action: {
            rewriteJSON(oldArrangement: load("statedata.json"))
        })

    }
}

Hopefully, that template helps anyone who's looking to clean up a JSON file to make it easier to read. It's a bit brute-force, but it gets the job done, and once you've renamed everything to fit your app or code the first time, subsequent adjustments would just involve a copy-and-pastes.

2      

Or you tell it to .prettyPrinted when encoding it

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted

See How to format JSON using Codable and pretty printing

2      

Just out of curiosity, why so much effort expended on getting a particular order of the JSON keys, when the order doesn't matter? Regardless of what order the keys are in, as long as your Codable types are set up correctly and your JSON is valid, everything works. That's kind of the beauty of Codable.

Or you tell it to .prettyPrinted when encoding it

Yeah, that helps with how it's displayed but it does nothing to change the order of the keys, which apparently is one of the things @DashingDave is trying to change.

@DashingDave, you can also set .outputFormatting = .sortedKeys to sort the JSON keys alphabetically, which still won't be in your preferred order but might make it easier to find particular keys if there are a lot of them.

2      

@roosterboy There's a lot of (different types of) data in the app I'm preparing, and some of it isn't complicated enough to bother maintaining a whole database for--it's just easier to enter in a text list. And then it's just much easier for me to interact with the data if it's organized in an order where properties flow in the same manner I'd expect to read information presented in an encyclopedia entry or wikipedia page. That's how I build my Structs, so having the data ordered the same way just makes it easier to find what I need. 8^)

Oh, and I forgot to mention this in the first post: the output text is compact JSON. I then use the free Mac app Boop to format it into readable form (and there are a multitude of json validators and such online that can do the same).

2      

@roosterBoy One example would be if you need to diff the codable text (without parsing it), e.g., to determine if A==B.

2      

That wouldn't require the keys to be in a particular order, just that the same order is used in both examples you are diffing. .outputFormatting = .sortedKeys takes care of that without a lot of extra work.

Not to mention there are online and command line tools that will diff two JSON strings regardless of key orders.

2      

TAKE YOUR SKILLS TO THE NEXT LEVEL If you like Hacking with Swift, you'll love Hacking with Swift+ – it's my premium service where you can learn advanced Swift and SwiftUI, functional programming, algorithms, and more. Plus it comes with stacks of benefits, including monthly live streams, downloadable projects, a 20% discount on all books, and free gifts!

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.