10

This question has been asked quite a few times over the years, but it has changed again in Swift 5, particularly in the last two betas.

Reading a JSON file seems to be quite simple:

func readJSONFileData(_ fileName: String) -> Array<Dictionary<String, Any>> {

    var resultArr: Array<Dictionary<String, Any>> = []

    if let url = Bundle.main.url(forResource: "file", withExtension: "json") {

        if let data = try? Data(contentsOf: url) {

            print("Data raw: ", data)

            if let json = try? (JSONSerialization.jsonObject(with: data, options: []) as! NSArray) {

                print("JSON: ", json)

                if let arr = json as? Array<Any> {

                    print("Array: ", arr)

                    resultArr = arr.map { $0 as! Dictionary<String, Any> }

                }

            }

        }

    }

    return resultArr

}

But writing is incredibly difficult, and all of the previous methods found on this site have failed in Swift 5 on Xcode 11 betas 5 and 6.

How can I write data to a JSON file in Swift 5?

I tried these approaches:

There weren't any errors except for deprecation warnings, and when I fixed those, it simply didn't work.

Jack Bashford
  • 43,180
  • 11
  • 50
  • 79
  • 2
    It hasn’t changed. Both process with `JSONSerialization` and `JSONEncoder` are the same as it’s always been. Rather than showing us the unrelated “reading” code that works fine, why don’t you show us the “writing” code that isn’t working for you? Or share a link or two for “all of the previous methods found on this site have failed”. – Rob Aug 26 '19 at 22:51
  • 2
    Needless to say, if you’re going to use methods that throw errors, use `try` rather than `try?`, wrap it in a `do`-`catch`, and print the `error` in the `catch`. The error throwing system will inform you precisely what’s going wrong. Maybe you’re trying to save to the bundle (which is read only). Maybe the object you’re trying to convert has some properties/values that cannot be represented in JSON. It’s impossible to say in the absence of a [reproducible example](https://stackoverflow.com/help/mcve). But if you’re not catching the errors, you’re flying blind. – Rob Aug 26 '19 at 23:00
  • I'd encourage to check the `Codable` for achieving it. Moreover, this might be useful https://medium.com/@ahmadfayyas/using-codables-for-persisting-custom-objects-in-userdefaults-aeb3625fbf36. – Ahmad F Aug 26 '19 at 23:16
  • @Rob Let's just say I have some JSON data - how would I write it to `file.json`? It's in my project directory - is this writable? And if not, how would I make it writable? – Jack Bashford Aug 26 '19 at 23:25
  • 1
    https://stackoverflow.com/questions/28768015/how-to-save-an-array-as-a-json-file-in-swift https://stackoverflow.com/questions/42550657/writing-json-file-programmatically-swift/42551387 https://www.youtube.com/watch?v=IoTt1orlGns https://forums.developer.apple.com/thread/92806 – Ahmad F Aug 26 '19 at 23:34
  • 1
    "Let's just say I have some JSON data - how would I write it to file.json?" What is the meaning of some JSON? Is it a dictionary or a plain text? – Ahmad F Aug 26 '19 at 23:37
  • Edited @Rob, any better? – Jack Bashford Aug 27 '19 at 00:00
  • @AhmadF in that context it's an array of dictionaries with string keys and values. – Jack Bashford Aug 27 '19 at 00:01
  • @Rob can you please repost the code in that comment? It looked quite useful. – Jack Bashford Aug 27 '19 at 00:03
  • 1
    @JackBashford It would be better if you edited your question to show *your attempt* and explain what happens rather than simply linking to other questions/answers – Paulw11 Aug 27 '19 at 00:13

3 Answers3

17

Let’s assume for a second that you had some random collection (either arrays or dictionaries or some nested combination thereof):

let dictionary: [String: Any] = ["bar": "qux", "baz": 42]

Then you could save it as JSON in the “Application Support” directory like so:

do {
    let fileURL = try FileManager.default
        .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        .appendingPathComponent("example.json")

    try JSONSerialization.data(withJSONObject: dictionary)
        .write(to: fileURL)
} catch {
    print(error)
}

For rationale why we now use “Application Support” directory rather than the “Documents” folder, see the iOS Storage Best Practices video or refer to the File System Programming Guide. But, regardless, we use those folders, not the Application’s “bundle” folder, which is read only.

And to read that JSON file:

do {
    let fileURL = try FileManager.default
        .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
        .appendingPathComponent("example.json")

    let data = try Data(contentsOf: fileURL)
    let dictionary = try JSONSerialization.jsonObject(with: data)
    print(dictionary)
} catch {
    print(error)
}

That having been said, we generally prefer to use strongly typed custom types rather than random dictionaries where the burden falls upon the programmer to make sure there aren’t typos in the key names. Anyway, we make these custom struct or class types conform to Codable:

struct Foo: Codable {
    let bar: String
    let baz: Int
}

Then we’d use JSONEncoder rather than the older JSONSerialization:

let foo = Foo(bar: "qux", baz: 42)
do {
    let fileURL = try FileManager.default
        .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        .appendingPathComponent("example.json")

    try JSONEncoder().encode(foo)
        .write(to: fileURL)
} catch {
    print(error)
}

And to read that JSON file:

do {
    let fileURL = try FileManager.default
        .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
        .appendingPathComponent("example.json")

    let data = try Data(contentsOf: fileURL)
    let foo = try JSONDecoder().decode(Foo.self, from: data)
    print(foo)
} catch {
    print(error)
}

For more information about preparing JSON from custom types, see the Encoding and Decoding Custom Types article or the Using JSON with Custom Types sample code.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • So, does the `create: true` create the file if it doesn't exist, or...? – Jack Bashford Aug 27 '19 at 04:59
  • You use `create: true` if the _folder_ might not exist. In the case of the application support directory, it isn’t created automatically unless you pass `true` for the `create` parameter. – Rob Aug 27 '19 at 05:00
  • And which argument list/method would I use if I wanted the file to be created if it doesn't exist? Sorry, just want to get my head around any potential issues that I could fix. – Jack Bashford Aug 27 '19 at 05:02
  • The `write` method already creates the file for you if it doesn’t already exist. – Rob Aug 27 '19 at 05:03
  • Awesome, thanks Rob! I'll test it and get back to you. – Jack Bashford Aug 27 '19 at 05:13
  • Hi Rob! How would I be able to *read* that `example.json` file that your code writes to? Writing seems to work, but I'd also like to read it just to get the data. – Jack Bashford Aug 27 '19 at 23:11
  • It’s the same process, but you use `JSONDecoder().decode(_:from:)` (or `JSONSerialization.jsonObject(with:)`). See expanded answer above. – Rob Aug 28 '19 at 00:19
  • I can write JSON with no problem, but when I run the function I wrote again, it just replaces the only entry. Before the `do` block I append to an array, and do `try JSONEncoder().encode(array).write(to: fileURL)`, but no avail. Any ideas? – fankibiber Apr 18 '20 at 14:11
  • @fankibiber - This writes the whole `array`. If the resulting JSON only has the new records, then that means that the `array` that you encoded must have only had new the new records, too. But, this is starting to get beyond the scope of this question, so I’d suggest you prepare a reproducible example (a [MCVE](https://stackoverflow.com/help/mcve)) and post your own question. Frankly, I suspect that in the preparation of the MCVE, you’re likely to find the source of the problem because it’s just not possible for the data to be in `array` but that only the new records showed up in the JSON. – Rob Apr 18 '20 at 14:27
  • @Rob, thank you for your reply. A while ago I made a thread about it: https://stackoverflow.com/questions/60745982/swift-how-to-add-more-than-one-item-to-json-array. Unfortunately I am unable to get to the root of the problem, it's probably something elementary, but no luck. :( – fankibiber Apr 18 '20 at 14:44
3

In case anyone is working with custom objects (as I am) and want a more 'all around' function with generics

Save File To Manager

func saveJSON<T: Codable>(named: String, object: T) {
    do {
        let fileURL = try FileManager.default
            .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
            .appendingPathComponent(named)
        let encoder = try JSONEncoder().encode(object)

        try encoder.write(to: fileURL)
    } catch {
        print("JSONSave error of \(error)")
    }
}

Read File From Manager

func readJSON<T: Codable>(named: String, _ object: T.Type) -> T? {
     do {
         let fileURL = try FileManager.default
                .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
                .appendingPathComponent(named)
         let data = try Data(contentsOf: fileURL)

         let object = try JSONDecoder().decode(T.self, from: data)

        return object
    } catch {
        return nil
    }
}
Misha Stone
  • 631
  • 7
  • 22
2

Answering the question:

Let's just say I have some JSON data - how would I write it to file.json? It's in my project directory - is this writable? And if not, how would I make it writable?

Assuming that "some JSON" means an array of dictionaries as [<String, String>], here is a simple example of how you could do it:

Consider that we have the following array that we need to write it as JSON to a file:

let array = [["Greeting":"Hello", "id": "101", "fruit": "banana"],
             ["Greeting":"Hola", "id": "102", "fruit": "tomato"],
             ["Greeting":"Salam", "id": "103", "fruit": "Grape"]]

The first thing to do is to convert it to a JSON (as Data instance). There is more that one option to do it, let's use JSONSerialization one:

do {
    let data = try JSONSerialization.data(withJSONObject: array, options: [])
    if let dataString = String(data: data, encoding: .utf8) {
        print(dataString)
    }
} catch {
    print(error)
}

At this point, you should see on the console:

[{"Greeting":"Hello","fruit":"banana","id":"101"},{"fruit":"tomato","Greeting":"Hola","id":"102"},{"fruit":"Grape","Greeting":"Salam","id":"103"}]

which is our data formatted as valid JSON.

Next, we need to write it on the file. In order to do it, we'll use FileManager as:

do {
    let data = try JSONSerialization.data(withJSONObject: array, options: [])
    if let documentDirectoryUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
        let fileUrl = documentDirectoryUrl.appendingPathComponent("MyFile.json")
        try data.write(to: fileUrl)
    }
} catch {
    print(error)
}

Note that the file should exist in the documents directory; In the above example its name should be "MyFile.json".

Ahmad F
  • 30,560
  • 17
  • 97
  • 143
  • Thank you for your answer Ahmad. One question - what is the documents directory? Can you provide a directory structure of an Xcode project, highlighting the directory structure please? – Jack Bashford Aug 27 '19 at 01:53