Search code examples
jsonswift3jsonserializer

Getting unexpected conversion from JSON to dictionary and back to JSON


I am trying to read a JSON file from myFile.json in Bundle, modify some elements and store it in the document directory as a JSON file. I thought that it would be a simple way to persist the data. Subsequently, I intend to read and write to document directory. The following code shows what I did and is heavily commented. It seems I am missing some essential step because the expected JSON conversions does not match the JSON spec. I am open to suggestions on how to I tested in the playground. The code is based on

Convert Dictionary to JSON in Swift

    import UIKit

/* Trying to read a json file from myFile.json in Bundle, modify some elemnts and store it
 in the document directory as a json file. Subsequently, intent to read and write to document directory

 myFile.json consistes of

 {
 "record": {"id": "A1234", "customer": "Customer-1"}
 }

*/


typealias MyRecord = [String: AnyObject]
var json:MyRecord!
let fileName = "myFile"
var dictionary = MyRecord()

func loadJsonFromBundle (forFilename fileName: String) -> MyRecord {

    if let url = Bundle.main.url(forResource: fileName, withExtension: "json") {
        if let data = NSData(contentsOf: url) {
            do {
                let dictionary = try JSONSerialization.jsonObject(with: data as Data, options: .allowFragments) as? [String:Any]
                print("dictionary = \(dictionary!)")
                /* displays
                 dictionary = ["record": {
                 customer = "Customer-1";
                 id = A1234;
                 }]
                */

                return dictionary! as MyRecord
            } catch {
                print("Error!! Unable to parse  \(fileName).json")
            }
        }
        print("Error!! Unable to load  \(fileName).json")
    }

    return [:]
}

func loadJsonFromDocument (forFilename fileName: String) -> MyRecord {

    let docDirectory = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
    if let url = docDirectory?.appendingPathComponent(fileName).appendingPathExtension("json") {
        if let data = NSData(contentsOf: url) {
            do {
                let dictionary = try JSONSerialization.jsonObject(with: data as Data, options: .allowFragments) as? [String:Any]
                print("dictionary = \(dictionary!)")
                return dictionary! as MyRecord
            } catch {
                print("Error!! Unable to parse  \(fileName).json")
            }
        }
        print("Error!! Unable to load  \(fileName).json")
    }

    return [:]
}


func saveJsonToFile (_ fileName:String, outString: String) -> URL {
    let docDirectory = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
    if let fileURL = docDirectory?.appendingPathComponent(fileName).appendingPathExtension("json") {
        print("fileURL = \(fileURL)")
        // Write to a file on disk

        do {
            try outString.write(to: fileURL, atomically: true, encoding: .utf8)
        } catch {
            print("Failed writing to URL: \(fileURL), Error: " + error.localizedDescription)
        }
        return fileURL
    }
    return URL(string: "")!
}

let sq = "\""
func q(_ x:String) -> String {
    return "\(sq)\(x)\(sq)"
}


dictionary = loadJsonFromBundle (forFilename: fileName)
var a = dictionary["record"] as? [String:String]
a?["customer"] = "newName"
var dict = MyRecord()
dict["record"] = a as AnyObject?
print(dict)

/* prints:

 ["record": {
 customer = newName;
 id = A1234;
 }]

*/

// https://stackoverflow.com/questions/29625133/convert-dictionary-to-json-in-swift/29628000#29628000
do {
    let jsonData = try JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted)
    // here "jsonData" is the dictionary encoded in JSON data

    let decoded = try JSONSerialization.jsonObject(with: jsonData, options: [])
    // here "decoded" is of type `Any`, decoded from JSON data

    // you can now cast it with the right type
    if let dictFromJSON = decoded as? [String:Any] {

        // need to save dictFromJson to a file in document directory
        // saveJsonToFile is expecting a String for the json argument
        // I converted dictFromJson to a string so I can save it
        var outString = String(describing: dictFromJSON)

        print("outString = \(outString)")

        /* Notice that the internal structure is not quoted and there are semi-colons

         outString = ["record": {
         customer = newName;
         id = A1234;
         }]

        */
        outString = outString.replacingOccurrences(of: "[", with: "{")
        outString = outString.replacingOccurrences(of: "]", with: "}")



        let url = saveJsonToFile("newFile", outString: String(describing: outString) )
        print(url)
        /* Resulting File looks like this:

         {"record": {
         customer = newName;
         id = A1234;
         }}

         Question: Why were the braces swapped with brackets. The internal elements
         were not quoted.

        */

        // Will try to read back the json string from document directory
        dictionary = loadJsonFromDocument(forFilename: fileName)
        // results in ERROR (Unable to load myFile.json


        a = dictionary["record"] as? [String:String]
        a?["customer"] = "newName"
        dict = MyRecord()
        dict["record"] = a as AnyObject?
        print(dict)

    }
} catch {
    print(error.localizedDescription)
}

Solution

  • Issue pointed by vadian is correct that you are storing the Dictionary object, but instead of converting Data to String and then write that String you can directly write Data in DocumentDirectory.

    So i have changed your saveJsonToFile function which accept Data as second argument instead of String.

    func saveJsonToFile (_ fileName:String, jsonData: Data) -> URL {
        let docDirectory = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        if let fileURL = docDirectory?.appendingPathComponent(fileName).appendingPathExtension("json") {
            print("fileURL = \(fileURL)")
            // Write to a file on disk
    
            do {
                try jsonData.write(to: fileURL)
            } catch {
                print("Failed writing to URL: \(fileURL), Error: " + error.localizedDescription)
            }
            return fileURL
        }
        return URL(string: "")!
    }
    

    Now simply call this function after you change your json result and convert that to data.

    do {
        let jsonData = try JSONSerialization.data(withJSONObject: dict, options: [])
        let url = saveJsonToFile("newFile", jsonData: jsonData )
        print(url)
    } catch {
        print(error.localizedDescription)
    }