在 Swift 中编写一个带有排序键的漂亮打印 JSON 对象

Posted

技术标签:

【中文标题】在 Swift 中编写一个带有排序键的漂亮打印 JSON 对象【英文标题】:Write a prettyPrinted JSON object with sorted keys in Swift 【发布时间】:2017-09-04 07:43:29 【问题描述】:

我们经常希望使用 JSON 来提高可读性。因此,通常要求按字母(或字母数字)对 JSON 键进行排序in Go、in .NET、in Python、in Java、...

但是如何在 Swift 中输出 JSON 键按字母顺序排序的 JSON?

PrettyPrinted 输出很简单:

JSONSerialization.writeJSONObject(jsonObject, to: outputStream, options: [.prettyPrinted], error: nil)

然而,为了便于阅读,这些键并未按字母顺序排序。它们可能按照NSDictionary.keyEnumerator() 给出的顺序排列。但遗憾的是,我们不能在 Swift 中继承 Dictionary、NSDictionary 或 CFDictionary,所以我们不能覆盖键顺序的行为。

[编辑:实际上,我们可以将 NSDictionary 子类化,请参阅下面我的答案之一]

【问题讨论】:

按照设计,字典不会以任何方式排序。如果顺序对您很重要,您应该使用数组。 @EmilioPelaez 这是人类可读性希望对键进行排序的常见模式。 在最新的(目前是测试版)SDK(ios 11、macOS 10.13 等)中,有一个.sortedKeys 写作选项。 【参考方案1】:

对于 iOS 11+ 和 macOS High Sierra (10.13+),新选项 .sortedKeys 可以轻松解决问题:

JSONSerialization.writeJSONObject(jsonObject, to: outputStream, options: [.sortedKeys, .prettyPrinted], error: nil)

感谢Hamish 的提示。

【讨论】:

【参考方案2】:

适用于 iOS 7+、macOS 10.9+(OS X Mavericks 及更高版本)的纯 Swift 解决方案。

一个解决方案是 subclass NSDictionary(但不要覆盖默认的 init 方法,因为它不能用 Swift 编译)。

class MutableOrderedDictionary: NSDictionary 
    let _values: NSMutableArray = []
    let _keys: NSMutableOrderedSet = []

    override var count: Int 
        return _keys.count
    
    override func keyEnumerator() -> NSEnumerator 
        return _keys.objectEnumerator()
    
    override func object(forKey aKey: Any) -> Any? 
        let index = _keys.index(of: aKey)
        if index != NSNotFound 
            return _values[index]
        
        return nil
    
    func setObject(_ anObject: Any, forKey aKey: String) 
        let index = _keys.index(of: aKey)
        if index != NSNotFound 
            _values[index] = anObject
         else 
            _keys.add(aKey)
            _values.add(anObject)
        
    

有了它,我们可以在使用.prettyPrinted 编写对象之前使用.forcedOrdering 对对象的键进行排序:

// force ordering
let orderedJson = MutableOrderedDictionary()
jsonObject.sorted  $0.0.compare($1.0, options: [.forcedOrdering, .caseInsensitive]) == .orderedAscending 
          .forEach  orderedJson.setObject($0.value, forKey: $0.key) 

// write pretty printed
_ = JSONSerialization.writeJSONObject(orderedJson, to: outputJSON, options: [.prettyPrinted], error: nil)

但要小心:如果有的话,您需要对 JSON 对象的 all 子字典进行子类化和排序。这是执行递归的扩展,灵感来自Evgen Bodunov 的gist(谢谢)。

extension MutableOrderedDictionary 
    private static let defaultOrder: ((String, Any), (String, Any)) -> Bool = 
        $0.0.compare($1.0, options: [.forcedOrdering, .caseInsensitive]) == .orderedAscending
    
    static func sorted(object: Any, by areInIncreasingOrder: ((key: String, value: Value), (key: String, value: Value)) -> Bool = defaultOrder) -> Any 
        if let dict = object as? [String: Any] 
            return MutableOrderedDictionary(dict, by: areInIncreasingOrder)
         else if let array = object as? [Any] 
            return array.map  sorted(object: $0, by: areInIncreasingOrder) 
         else 
            return object
        
    
    convenience init(_ dict: [String: Any], by areInIncreasingOrder: ((key: String, value: Value), (key: String, value: Value)) -> Bool = defaultOrder) 
        self.init()
        dict.sorted(by: areInIncreasingOrder)
            .forEach  setObject(MutableOrderedDictionary.sorted(object: $0.value, by: areInIncreasingOrder), forKey: $0.key) 
    

用法:

// force ordering
let orderedJson = MutableOrderedDictionary(jsonObject)

// write pretty printed
_ = JSONSerialization.writeJSONObject(orderedJson, to: outputJSON, options: [.prettyPrinted], error: nil)

【讨论】:

很好的答案。我已经更新它以使用递归对象并方便地初始化。 gist.github.com/molind/94b750bdd6c603d608b2310f6be5f050【参考方案3】:

适用于 iOS 7+、macOS 10.9+(OS X Mavericks 及更高版本)的解决方案。

解决方案是在 Objective-C 中子类化 NSDictionary,然后使用框架(用于应用程序)或静态库(用于命令行工具)中的子类。

对于这个演示,我将使用nicklockwood/OrderedDictionary(700+ 行代码)而不是从头开始,但可能有未经测试的替代方案,如quinntaylor/CHOrderedDictionary 或rhodgkins/RDHOrderedDictionary。要将其集成为框架,请在您的 PodFile 中添加此依赖项:

pod 'OrderedDictionary', '~> 1.4'

然后我们将排序对象的键:

import OrderedDictionary

let orderedJson = MutableOrderedDictionary()
jsonObject.sorted  $0.0.compare($1.0, options: [.forcedOrdering, .caseInsensitive]) == .orderedAscending 
          .forEach  orderedJson.setObject($0.value, forKey: $0.key) 

(注意:setObject(_,forKey:) 特定于 MutableOrderedDictionary) 最后我们可以把它写成 prettyPrinted:

_ = JSONSerialization.writeJSONObject(orderedJson, to: outputJSON, options: [.prettyPrinted], error: nil)

但请注意:您需要对 JSON 对象的所有子字典进行子类化和排序。

【讨论】:

【参考方案4】:

这是一种可能的解决方法,仅适用于 macOS Swift 脚本。它不适用于 iOS

我们使用不同的编程语言(例如Python 2.7)解决了 Swift Foundation 的限制。

import Cocoa

// sample jsonObject
let jsonObject = ["hello": "world", "foo": "bar"]

// writing JSON to file
let jsonPath = "myJson.json"
let outputJSON = OutputStream(toFileAtPath: jsonPath, append: false)!
outputJSON.open()
_ = JSONSerialization.writeJSONObject(jsonObject, to: outputJSON, options: [], error: nil)
outputJSON.close()

// sortedKeys equivalent using `sort_keys=True`
// prettyPrinted equivalent using `indent=2, separators=(',', ' : ')`
// unicode using `io.open` and `ensure_ascii=False`
func shell(_ args: String...) 
    let task = Process()
    task.launchPath = "/usr/bin/env"
    task.arguments = args
    task.launch()
    task.waitUntilExit()

shell("python", "-c", ""
    + "import io\n"
    + "import json\n"
    + "jsonPath = 'myJson.json'\n"
    + "with io.open(jsonPath, mode='r', encoding='utf8') as json_file:\n"
    + "    all_data = json.load(json_file)\n"
    + "with io.open(jsonPath, mode='w', encoding='utf8') as json_file:\n"
    + "    json_file.write(unicode(json.dumps(all_data, ensure_ascii=False, sort_keys=True, indent=2, separators=(',', ' : '))))\n"
)

【讨论】:

以上是关于在 Swift 中编写一个带有排序键的漂亮打印 JSON 对象的主要内容,如果未能解决你的问题,请参考以下文章

在 NSObject 的子类上实现啥方法以允许在 swift 操场上进行漂亮的打印?

复数的漂亮打印

如何在多图中按排序顺序打印键的值

在 Emacs 上漂亮地打印 XML 文件

带有 Qt 漂亮打印机的 gdb

swift 漂亮的打印Json对象