Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ordered Dictionary in JSON

Tags:

json

swift

There are 3 string variables

public var userLoginId : String?
public var searchString : String?
public var tableName : String?

I have a dictionary:

let dict = ["userLoginId" : userLoginId, "searchString" : searchString,"tableName" : tableName]

Now I serialize it by JSON:

let data =  try! JSONSerialization.data(withJSONObject: dict, options :[])
let jsonstring = String(data:data, encoding:.utf8)!

In jsonstring, every time I get an unordered JSON like below:

"{\"tableName\":\"null\",\"userLoginId\":\"Dilip\",\"searchString\":\"Tata\"}"

How can I get in the same format in which I assigned value to dictionary?

like image 632
Dilip Jangid Avatar asked Sep 25 '26 17:09

Dilip Jangid


2 Answers

Dictionaries are unordered

đź’ˇBut !

Since JSON is a String you can have an ordered JSON. You can use this library to parse the JSON in the original order. The usage is simple:

try JSON.parse(string: jsonString)

Sorted Dictionary

From iOS 11 and macOS 10.13 there is a new option for JSONSerialization for sorting keys that you can use:

JSONSerialization.data(withJSONObject: json, options: [.sortedKeys])
like image 55
Mojtaba Hosseini Avatar answered Sep 28 '26 23:09

Mojtaba Hosseini


Maintain JSON object key order in swift

Once you get over everyone telling you you can't, or its wrong, or you can't rely on a JSON object's order because of the specification…etc. You can just go ahead and do the impossible as follows:

Add a dependency to swift-collections in your Package.swift so we have access to OrderedDictionary.

Go grab a copy of JSONSerialization+Parser.swift from swift-corelibs-foundation and put it somehwere in your project.

Using curl:

curl -L -o "${YOUR_PROJECT_SOURCE_DIR}/JSONSerialization+Parser.swift" 'https://raw.githubusercontent.com/apple/swift-corelibs-foundation/c284a2390b9050386758e36e4c2a2b648493efd9/Sources/Foundation/JSONSerialization%2BParser.swift'

Or download manually from GitHub (Note: my patch was created against commit c284a23) : Permalink to JSONSerialization+Parser.swift (commit c284a23)

Apply the following changes to JSONSerialization+Parser.swift

@@ -9,7 +9,8 @@
 // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
 //
 //===----------------------------------------------------------------------===//
-
+import OrderedCollections
+import Foundation

 internal struct JSONParser {
     var reader: DocumentReader
@@ -142,7 +143,7 @@

     // MARK: - Object parsing -

-    mutating func parseObject() throws -> [String: JSONValue] {
+    mutating func parseObject() throws -> OrderedDictionary<String, JSONValue> {
         precondition(self.reader.read() == ._openbrace)
         guard self.depth < 512 else {
             throw JSONError.tooManyNestedArraysOrDictionaries(characterIndex: self.reader.readerIndex - 1)
@@ -162,7 +163,7 @@
             break
         }

-        var object = [String: JSONValue]()
+        var object = OrderedDictionary<String, JSONValue>()
         object.reserveCapacity(20)

         while true {
@@ -663,3 +664,29 @@
     case singleFragmentFoundButNotAllowed
     case invalidUTF8Sequence(Data, characterIndex: Int)
 }
+
+enum JSONValue: Equatable {
+    case null
+    case array([JSONValue])
+    case object(OrderedDictionary<String, JSONValue>)
+    case bool(Bool)
+    case string(String)
+    case number(String)
+
+    func unwrap() throws -> Any {
+        switch self {
+        case .null: return NSNull()
+        case let .array(values): return try values.map { try $0.unwrap() }
+        case let .object(object): return try object.mapValues { try $0.unwrap() }
+        case let .bool(bool): return NSNumber(value: bool)
+        case let .string(string): return string
+        case let .number(string):
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            guard let number = formatter.number(from: string) else {
+                throw JSONError.numberIsNotRepresentableInSwift(parsed: string)
+            }
+            return number
+        }
+    }
+}

You can then use it like this:

import Foundation
import OrderedCollections

@main
public enum swift_json_ordered_dictionary {
    public static func main() {
        let jsonStr = """
        {
            "name": "John Doe",
            "age": 30,
            "orderMatters": "Sometimes order matters even when the world is telling you you're wrong",
            "inOrder": "Look at me ma, I've got myself in order",
            "ducksInARow": "Having all your ducks in a row",
            "favorites": {
                "color": "Rainbow",
                "food": "Pizza",
                "animal": "Unicorn",
                "activity": "Napping"
            },
            "programmingLanguages": ["Swift", "Python", "JavaScript", "Gibberish"],
            "job": "Developer",
            "quote": "Debugging is like being the detective in a crime movie where you are also the murderer",
            "nestedObjects": {
                "nested1": {
                    "message": "Order matters, even in nested objects!"
                },
                "nested2": {
                    "message": "Embrace the chaos, but keep things in order."
                },
                "nested3": {
                    "message": "Defying conventions, because order is a beautiful thing."
                }
            }
        }
        """
        do {
            var parser = JSONParser(bytes: Array(jsonStr.utf8))
            let jsonValue = try parser.parse()
            let x = try jsonValue.unwrap()
            if let dict = x as? OrderedDictionary<String, Any> {
                print(dict.prettyPrint())
            }
        } catch { print("\(error)") }
    }
}

Note: the .prettyPrint() function is just an extension I added to OrderedDictionary to give a nice output like this:

{
  "name": "John Doe",
  "age": 30,
  "orderMatters": "Sometimes order matters even when the world is telling you you're wrong",
  "inOrder": "Look at me ma, I've got myself in order",
  "ducksInARow": "Having all your ducks in a row",
  "favorites": {
    "color": "Rainbow",
    "food": "Pizza",
    "animal": "Unicorn",
    "activity": "Napping"
  },
  "programmingLanguages": [
    "Swift",
    "Python",
    "JavaScript",
    "Gibberish",
  ],
  "job": "Developer",
  "quote": "Debugging is like being the detective in a crime movie where you are also the murderer",
  "nestedObjects": {
    "nested1": {
      "message": "Order matters, even in nested objects!"
    },
    "nested2": {
      "message": "Embrace the chaos, but keep things in order."
    },
    "nested3": {
      "message": "Defying conventions, because order is a beautiful thing."
    }
  }
}

And as you can see, order was maintained with very minimal changes to JSONSerialization+Parser.swift.

Here is a little helper to apply the changes above to JSONSerialization+Parser.swift in a single command:

patch "${YOUR_PROJECT_SOURCE_DIR}/JSONSerialization+Parser.swift" < <(cat <<'EOF'
@@ -9,7 +9,8 @@
 // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
 //
 //===----------------------------------------------------------------------===//
-
+import OrderedCollections
+import Foundation

 internal struct JSONParser {
     var reader: DocumentReader
@@ -142,7 +143,7 @@

     // MARK: - Object parsing -

-    mutating func parseObject() throws -> [String: JSONValue] {
+    mutating func parseObject() throws -> OrderedDictionary<String, JSONValue> {
         precondition(self.reader.read() == ._openbrace)
         guard self.depth < 512 else {
             throw JSONError.tooManyNestedArraysOrDictionaries(characterIndex: self.reader.readerIndex - 1)
@@ -162,7 +163,7 @@
             break
         }

-        var object = [String: JSONValue]()
+        var object = OrderedDictionary<String, JSONValue>()
         object.reserveCapacity(20)

         while true {
@@ -663,3 +664,29 @@
     case singleFragmentFoundButNotAllowed
     case invalidUTF8Sequence(Data, characterIndex: Int)
 }
+
+enum JSONValue: Equatable {
+    case null
+    case array([JSONValue])
+    case object(OrderedDictionary<String, JSONValue>)
+    case bool(Bool)
+    case string(String)
+    case number(String)
+
+    func unwrap() throws -> Any {
+        switch self {
+        case .null: return NSNull()
+        case let .array(values): return try values.map { try $0.unwrap() }
+        case let .object(object): return try object.mapValues { try $0.unwrap() }
+        case let .bool(bool): return NSNumber(value: bool)
+        case let .string(string): return string
+        case let .number(string):
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            guard let number = formatter.number(from: string) else {
+                throw JSONError.numberIsNotRepresentableInSwift(parsed: string)
+            }
+            return number
+        }
+    }
+}

EOF
)

For easier testing I've added the ...PrettyPrint funcs (neither fine nor fast, but working :-)):

fileprivate func JsonPrettyPrint(_ value: Any, offset: String = "") -> String {
    var result: String = ""
    switch value {
    case is NSNull: result += ""; break
    case let asArray as [Any]: result += asArray.jsonPrettyPrint(); break
    case let asOrderedDict as OrderedDictionary<String, Any>: result += asOrderedDict.jsonPrettyPrint(offset: offset); break
    case let asNSNumber as NSNumber: result += asNSNumber.stringValue; break
    case let asString as String: result += "\"\(asString)\""; break
    default: result += "???"; break
    }
    return result
}

extension Array where Element == Any {
    func jsonPrettyPrint(offset: String = "") -> String {
        var result: String = ""
        for value in self {
            if !result.isEmpty { result += ", " }
            result += JsonPrettyPrint(value, offset: offset + "")
        }
        return "[" + result + "]"
    }
}

extension OrderedDictionary where Key == String, Value == Any {
    func jsonPrettyPrint(offset: String = "") -> String {
        var result: String = ""
        for (key, value) in self {
            if !result.isEmpty { result += ",\n" }
            result += offset + "\t" + "\"\(key)\": " + JsonPrettyPrint(value, offset: offset + "\t")
        }
        return "{\n" + result + "\n\(offset)}"
    }
}
like image 27
Chris Avatar answered Sep 28 '26 23:09

Chris