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

Attempt to restructure JSON data using Swift

Forums > SwiftUI

Last July, I posted about searching for a way to conveniently restructure a JSON file (such as adding a new key/parameter to every object currently in the file if I thought of another piece of data I wanted to add to all the entries) rather than using a complex Find/Replace in a text editor.

I only got one notification that there were updates to the topic, so I never checked after my second post until this year, when I was looking to add to it again on Tuesday rather than create a new topic on the forum.

Many thanks to @Obelix for all the effort undertaken to provide help! As I don't know Javascript and am rather uncomfortable with CLIs, it would take weeks longer to implement one of the possible solutions than the few hours of using Regular Expressions in TextEdit to restructure the data. The only suggested solution I'm currently capable of attempting is using Swift itself to restructure the data.

So in this instance, my goal is to add 2 new parameters and place the old cities array into a sublevel, converting this

[
    {
        "name": "Oklahoma",
        "cities": ["Norman", "Oklahoma City", "Tulsa"],
        "yearOfStatehood": 1907
    },
    {
        "name": "Alabama",
        "cities": ["Birmingham", "Huntsville", "Montgomery"],
        "yearOfStatehood": 1819
    },
    {
        "name": "Michigan",
        "cities": ["Detroit", "Grand Rapids", "Warren City"],
        "yearOfStatehood": 1837
    }
]

into this

[
    {
        "name": "Oklahoma",
        "cities": [
            {
                "name": "Norman",
                "population": 130 000
            },
            {
                "name": "Oklahoma City",
                "population": 700 000
            },
            {
                "name": "Tulsa",
                "population": 410 000
            }
        ],
        "stateFlag": "oklahoma.png",
        "yearOfStatehood": 1907
    },
    {
        ...
    },
    {
        ...        
    }
]

I've omitted the data here for the last two states to try to reduce the scroll length of this post.

Given all that, here's my attempt:

struct OldStateStructure: Codable {
    let name: String

    let cities: [String]

    let yearOfStatehood: Int
}

struct NewStateStructure: Codable {
    var name: String

    var cities: [City]

    struct City: Codable {
        var name: String
        var population: Int
    }

    var stateFlag: String
    var yearOfStatehood: Int  
}

And the following is the function I've attempted to create to produce the change (prior to somehow rewriting the JSON file itself), but I get an error on the first line of the outer ForEach loop (an error that would presumably repeat on multiple lines in the subsequent code):

"No exact matches in reference to static method 'buildExpression'"

func convertJSON(oldData: [OldStateStructure]) -> [NewStateStructure] {
    var newState: NewStateStructure
    var newStateArray: [NewStateStructure] = []

    ForEach(oldData, id: \.name) { object in
        newState.name = object.name      // No exact matches in reference to static method 'buildExpression'

        newState.cities {
            var newCity: NewStateStructure.City
            var newCityArray: [NewStateStructure.City] = []

            ForEach(object.cities, id: \.self) { city in
                newCity.name = city
                newCity.population = 0
                newCityArray.append(newCity)
            }

            return newCityArray
        }

        newState.stateFlag = ""
        newState.yearOfStatehood = 0

        newStateArray.append(newState)
    }

    return newStateArray
}

Based on what I was able to find on the internet (I found 5 cases of this error), the ForEach seems to be the issue, but there were other cases that produced the error, so I'm not sure. Even if it is the ForEach, I'm stumped as to what it's telling me that I'm doing incorrectly. Is the error because there's no View type in the loop? (Using for-in loops instead removes the error.)

There's apparently another issue with the newState.cities closure, which has been the most difficult part of this, but I'm still trying to figure that one out on my own. I was trying to create a calculated variable, but maybe that's how it should be done.

2      

Is the error because there's no View type in the loop? (Using for-in loops instead removes the error.)

Exactly so.

There's apparently another issue with the newState.cities closure, which has been the most difficult part of this, but I'm still trying to figure that one out on my own. I was trying to create a calculated variable, but maybe that's how it should be done.

I don't understand why you have this as a closure. NewStateStructure.cities isn't a closure, it's an array.

You just need to loop through oldData.cities and create a new City struct for each item, which you then add to newState.cities.

2      

Here is a quick and dirty example I whipped up to show you one way you could do it. I use map instead of for in loops.

(You can essentially ignore everything except the convertJSON function; all that other stuff is just setup to give us a working example.)

import Foundation

struct OldStateStructure: Codable {
    let name: String

    let cities: [String]

    let yearOfStatehood: Int
}

struct NewStateStructure: Codable {
    var name: String

    var cities: [City]

    struct City: Codable {
        var name: String
        var population: Int
    }

    var stateFlag: String
    var yearOfStatehood: Int
}

let oldStateJSONData = """
[
    {
        "name": "Oklahoma",
        "cities": ["Norman", "Oklahoma City", "Tulsa"],
        "yearOfStatehood": 1907
    },
    {
        "name": "Alabama",
        "cities": ["Birmingham", "Huntsville", "Montgomery"],
        "yearOfStatehood": 1819
    },
    {
        "name": "Michigan",
        "cities": ["Detroit", "Grand Rapids", "Warren City"],
        "yearOfStatehood": 1837
    }
]
""".data(using: .utf8)!

func convertJSON(oldData: [OldStateStructure]) -> [NewStateStructure] {
    oldData.map { oldState in
        let cities = oldState.cities.map { city in
            NewStateStructure.City(name: city, population: 0)
        }
        return NewStateStructure(name: oldState.name,
                                 cities: cities,
                                 stateFlag: "",
                                 yearOfStatehood: 0)
    }
}

let oldStates: [OldStateStructure]
do {
    oldStates = try JSONDecoder().decode([OldStateStructure].self, from: oldStateJSONData)
} catch {
    print(error)
    fatalError()
}
print(oldStates)

print(convertJSON(oldData: oldStates))

Another way to do it would be to create an initializer on NewStateStructure that takes in an OldStateStructure and knows how to convert it.

//so add this extension to NewStateStructure:
extension NewStateStructure {
    init(from oldState: OldStateStructure) {
        self.name = oldState.name
        self.cities = oldState.cities.map { city in
            City(name: city, population: 0)
        }
        self.stateFlag = ""
        self.yearOfStatehood = oldState.yearOfStatehood
    }
}

//and then the function becomes this:
func convertJSON(oldData: [OldStateStructure]) -> [NewStateStructure] {
    oldData.map(NewStateStructure.init)
}

This is how I would do it. The NewStateStructure knows how to create itself given an OldStateStructure, so all that functionality is encapsulated where it belongs instead of being handled externally in some other function. Much cleaner that way.

Although, I do feel it somewhat important to point out that the function convertJSON is not actually converting any JSON. It is converting an array of OldStateStructures to an array of NewStateStructures. JSON doesn't enter into it anywhere. Maybe the original array has been decoded from JSON and maybe the output array will be encoded to JSON, but those activities take place outside this function. Don't think of your structs as JSON, because they are not. Your structs are the actual data objects; JSON is just how your data is stored and transported.

Heck, you may not even need the function at all, depending on the rest of your code:

do {
    let oldStates = try JSONDecoder().decode([OldStateStructure].self, from: oldStateJSONData)
    let newStates = oldStates.map(NewStateStructure.init)

    let enc = JSONEncoder()
    enc.outputFormatting = .prettyPrinted
    let newStateJSONData = try enc.encode(newStates)
    print(String(data: newStateJSONData, encoding: .utf8)!)
} catch {
    print(error)
    fatalError()
}

Something to think about...

2      

Wow--that's gonna take me a while to work through; thank you!

Although I'm stumped--where are the do and the solo print statements supposed to go? Don't they need to go inside functions?

2      

Although I'm stumped--where are the do and the solo print statements supposed to go? Don't they need to go inside functions?

I wrote that code in a playground, so code that is not contained within a function is executed from top to bottom. It is purely an example of how to write the convertJSON function, which was the purpose of the exercise. In your own code, you would call the function from wherever it needs to be called; you would not use a standalone do or print like that.

2      

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

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.