Swiftのちょっと便利なJSONのencoderを作った

Swiftの`Codable`は便利ですが、`Foundation`の`JSONEncoder`, `JSONDecoder`を使っていて微妙に不便に感じる事があります。自分がいろいろな案件をこなしたり、他人の困り事を聞いてきた上で、機能強化したencoderについてのアイデアが浮かんだので作りました。

ライブラリの名前は`FineJSON`です。その名の通り、`JSONEncoder`と`JSONDecoder`のちょっと良い代替として設計されており、`FineJSONEncoder`と`FineJSONDecoder`を提供します。

リポジトリはこちらです。https://github.com/omochi/FineJSON

# 機能紹介

`FineJSON`の機能を紹介します。

## ケツカンマを許容する

Decoderはケツカンマを無視します。設定ファイルなどにJSONを使っていると、よくこのミスでハマるんですよね。

```swift
    struct A : Codable, Equatable {
        var a: Int
        var b: Int
    }

    func testAllowTrailingComma() throws {
        let json = """
[
  {
    "a": 1,
    "b": 2,
  },
]
"""
        let decoder = FineJSONDecoder()
        let x = try decoder.decode([A].self, from: json.data(using: .utf8)!)
        XCTAssertEqual(x, [A(a: 1, b: 2)])
    }
```

## コメントを許容する

Decoderはコメントを許容します。ラインコメントとブロックコメントに対応しています。

```swift
    struct A : Codable, Equatable {
        var a: Int
        var b: Int
    }

    func testComment() throws {
        let json = """
[
  // entry 1
  {
    "a": 10,
    "b": 20
/*
    "a": 1,
    "b": 2,
*/
  }
]
"""
        let decoder = FineJSONDecoder()
        let x = try decoder.decode([A].self, from: json.data(using: .utf8)!)
        XCTAssertEqual(x, [A(a: 10, b: 20)])
    }
```

## パースエラーが位置情報を持っている

パースエラーがJSONにおける位置情報を持っています。行番号、列番号(byte換算), バイトオフセットが得られます。JSONファイルが4000行とかあると、CodingKeysで「usersキーの要素の5番目の要素のitems要素の10番目の要素でエラーです」とか言われても、それどこなん、ってなりますよね。

```swift
    struct A : Codable, Equatable {
        var a: Int
        var b: Int
    }

    func testParseErrorLocation() throws {
        let json = """
[
  {
    "a": 1,
    "b": 2;
  }
]
"""
        let decoder = FineJSONDecoder()
        do {
            _ = try decoder.decode([A].self, from: json.data(using: .utf8)!)
            XCTFail()
        } catch {
            let message = "\(error)"
            // invalid character (";") at 4:11(28)
            XCTAssertTrue(message.contains("4:11(28)"))
        }
    }
```

## デコーダから位置情報を取り出せる

位置情報を`Decoder`から取り出す事ができます。デコードはできたけど処理してみたらエラーになった、みたいなとき、何行目のエントリで問題が起こったよ、みたいな事を自分で実装できます。

```swift
    struct B : Decodable {
        var location: SourceLocation?
        var name: String
        enum CodingKeys : String, CodingKey { case name }
        init(from decoder: Decoder) throws {
            self.location = decoder.sourceLocation
            let c = try decoder.container(keyedBy: CodingKeys.self)
            self.name = try c.decode(String.self, forKey: .name)
        }
    }

    func testDecodeLocation() throws {
        let json = """
// comment
{
  "name": "b"
},
"""
        let decoder = FineJSONDecoder()
        let x = try decoder.decode(B.self, from: json.data(using: .utf8)!)
        XCTAssertEqual(x.location, SourceLocation(offset: 11, line: 2, columnInByte: 1))
        XCTAssertEqual(x.name, "b")
    }
```

この情報の自動デコード機能もあります。後述。

## JSONのキーの順序を維持する

標準のencoderでは、オブジェクトのプロパティをエンコードした順番と、出力されるJSONにおけるキーの順番は対応付けされません。API呼び出しのリクエストなど、それが全く問題にならない場面もありますが、設定ファイルなど、人間が読み書きするJSONにおいてはそれだと不便です。FineJSONでは内部で順序付き辞書を持っていて、エンコードした順番でキーが並んだJSONを出力できます。


```swift
    struct A : Codable {
        var a: Int
        var b: String
        var c: Int?
        var d: String?
    }
    
    func testKeyOrder() throws {
        let a = A(a: 1, b: "b", c: 2, d: "d")
        let e = FineJSONEncoder()
        let json = String(data: try e.encode(a), encoding: .utf8)!
        let expected = """
{
  "a": 1,
  "b": "b",
  "c": 2,
  "d": "d"
}
"""
        XCTAssertEqual(json, expected)
    }
```

## `Optional.none`のエンコーディング方法を制御する

`Optional`なプロパティが`none`だった時、デフォルトではJSONのキーごと削除されますが、`FineJSON`では、キーとともにJSONの`null`を出力する挙動を選択できます。

```swift
    func testNoneExplicitNull() throws {
        let a = A(a: 1, b: "b", c: nil, d: "d")
        let e = FineJSONEncoder()
        e.optionalEncodingStrategy = .explicitNull
        let json = String(data: try e.encode(a), encoding: .utf8)!
        let expected = """
{
  "a": 1,
  "b": "b",
  "c": null,
  "d": "d"
}
"""
        XCTAssertEqual(json, expected)
    }
```

## インデントの深さを制御する

インデントの深さをデフォルトのスペース2つから、4つやタブ文字に変更できます。

```swift
    func testIndent4() throws {
        let a = A(a: 1, b: "b", c: 2, d: "d")
        let e = FineJSONEncoder()
        e.jsonSerializeOptions = JSON.SerializeOptions(indentString: "    ")
        let json = String(data: try e.encode(a), encoding: .utf8)!
        let expected = """
{
    "a": 1,
    "b": "b",
    "c": 2,
    "d": "d"
}
"""
        XCTAssertEqual(json, expected)
    }
```

1行形式ももちろん使えます。

```swift
    func testOnelineFormat() throws {
        let a = A(a: 1, b: "b", c: 2, d: "d")
        let e = FineJSONEncoder()
        e.jsonSerializeOptions = JSON.SerializeOptions(isPrettyPrint: false)
        let json = String(data: try e.encode(a), encoding: .utf8)!
        let expected = """
{"a":1,"b":"b","c":2,"d":"d"}
"""
        XCTAssertEqual(json, expected)
    }
```

`Foundation`と異なり、デフォルトを見やすい形式の方にしています。

## 無限精度のnumberを扱う

本来JSONはその文法仕様上、numberの桁数に制限はなく、無限精度がシリアライズできます。しかし、`Foundation`ではデコードした時にそれを内部的に有限精度で格納してしまうため、せっかくのJSONの表現能力を失ってしまいます。また、JSONデータ上で`0.1`と書かれていたような場合でも、この数値は2進浮動少数では表現できないので、0.1にとても近いが0.1では無い数になってしまいます。例えば、下記のような事が起こります。

```swift
let e = JSONEncoder()
let r = String(data: try e.encode([Float(0.1)]), encoding: .utf8)!
print(r) // => [0.10000000149011612]
```

また、単純に桁数が大きくなるとビット幅の表現能力を超えてしまい、末尾の方の桁の値が壊れてしまう事もあります。

`FineJSON`では、内部表現が文字列になっている`JSONNumber`という型を提供しており、これを使うことで、JSONデータ上の文字列表現のままで数値を取り扱えます。これに合わせてユーザ側で適切なライブラリを用いることで、JSON上の表現を損なう事なく操作できます。下記の例では内部的に10進表現をもつ`Decimal`型を使って値を変更しています。

```swift
    struct B : Codable {
        var x: JSONNumber
        var y: JSONNumber
    }

    func testArbitraryNumber() throws {
        let json1 = """
{
  "x": 1234567890.1234567890,
  "y": 0.01
}
"""
        let d = FineJSONDecoder()
        var b = try d.decode(B.self, from: json1.data(using: .utf8)!)
        
        var y = Decimal(string: b.y.value)!
        y += Decimal(string: "0.01")!
        b.y = JSONNumber(y.description)
        
        let e = FineJSONEncoder()        
        let json2 = String(data: try e.encode(b), encoding: .utf8)!
        
        let expected = """
{
  "x": 1234567890.1234567890,
  "y": 0.02
}
"""
        XCTAssertEqual(json2, expected)
    }
```

## プリミティブな値のデコードにおける弱い型付け

JSONにおいて数値と文字列は区別されていますが、現実世界では残念ながら、時折この違いを考慮していない困ったJSONデータに出会う事があります。`FineJSON`ではこれらを相互に暗黙変換してデコードするので、そういったJSONデータでもそのまま読み込めます。

```swift
    struct C : Codable {
        var id: Int
        var name: String
    }

    func testWeakTypingDecoding() throws {
        let json = """
{
  "id": "123",
  "name": 4869.57
}
"""
        let d = FineJSONDecoder()
        let c = try d.decode(C.self, from: json.data(using: .utf8)!)
        
        XCTAssertEqual(c.id, 123)
        XCTAssertEqual(c.name, "4869.57")
    }
```

通信の世界は「厳密に書いて柔軟に受け取る」のが良いという考え方があり、JSONエンコーディングについてもそう言える場合があると思います。ただ、気に食わない場合は`CodablePrimitiveJSONDecoder`プロトコルのオブジェクトを注入することで挙動を変更できるようにしてあります。

## 複雑な構造のJSONをそのまま扱う

`FineJSON`が提供している`JSON`型を使えば`Codable`で扱えないような複雑な構造も扱えます。`Any`型でも似たようなことはできますが、`Any`型は本当になんでも格納できるため緩すぎて不便です。`JSON`型は、JSONに定義された6つの型のenumなので適度な硬さで扱えます。

```swift
    struct F : Codable {
        var name: String
        var data: JSON
    }
    
    func testJSONTypeProperty() throws {
        let json = """
{
  "name": "john",
  "data": [
    "aaa",
    { "bbb": "ccc" }
  ]
}
"""
        let d = FineJSONDecoder()
        let f = try d.decode(F.self, from: json.data(using: .utf8)!)
      
        XCTAssertEqual(f.name, "john")
        XCTAssertEqual(f.data, JSON.array(JSONArray([
            .string("aaa"),
            .object(JSONObject([
                "bbb": .string("ccc")
                ]))
            ])))
    }
```

## 特定のプロパティだけJSONでのキーを変更する

通常の`Codable`においてJSONのキーをプロパティ名から変更しようとした場合、`CodingKeys`を自分で書かねばなりません。

```swift
struct G : Codable {
    enum CodingKeys : String, CodingKey {
        case id = "no"
        case point
        case userName = "user_name"
    }
    
    var id: Int
    var point: Int
    var userName: String
}
```

本来的に意図しているのは、「`id`プロパティは`no`キー, `userName`プロパティは`user_name`キーを使う」という事であるのに対して、`case point`も書かねばならないのがいただけません。関心のオーダーと作業量のオーダーが一致していないからです。この例では`point`の1つだけですが、これがもし20個のプロパティがあったら、`case`を書くのが面倒です。

`Foundation.JSONEncoder`には、`keyEncodingStrategy`というものがありますが、`.convertToSnakeCase`では対応できない場合もありますし、`.custom`の仕様は設定が型の定義箇所と分断するので、関心とコードの局所性の不一致をもたらすため使いにくいと思います。

`FineJSON`では、下記のように対象の型に対して`JSONAnnotatable`プロトコルを適用して、`keyAnnotations`プロパティを定義することで、`Codable`の自動生成機能が有効であるままで、JSONのキーを変更することができます。

```swift
    struct G : Codable, JSONAnnotatable {
        static let keyAnnotations: JSONKeyAnnotations = [
            "id": JSONKeyAnnotation(jsonKey: "no"),
            "userName": JSONKeyAnnotation(jsonKey: "user_name")
        ]
        
        var id: Int
        var point: Int
        var userName: String
    }
    
    func testAnnotateJSONKey() throws {
        let json1 = """
{
  "no": 1,
  "point": 100,
  "user_name": "john"
}
"""
        let d = FineJSONDecoder()
        var g = try d.decode(G.self, from: json1.data(using: .utf8)!)
        
        XCTAssertEqual(g.id, 1)
        XCTAssertEqual(g.point, 100)
        XCTAssertEqual(g.userName, "john")
        
        g.point += 3
        
        let e = FineJSONEncoder()
        let json2 = String(data: try e.encode(g), encoding: .utf8)!
        
        let expect = """
{
  "no": 1,
  "point": 103,
  "user_name": "john"
}
"""
        XCTAssertEqual(json2, expect)
    }
```

## キーがない場合のデフォルト値の指定

設定ファイルのJSONを`Codable`で扱っている場合などに、設定項目が追加された時、単純に型にプロパティを追加してしまうと、その新しいプロパティが含まれていない古い設定ファイルが読み込めなくなってしまうため、プロパティの型を`Optional`にしなければならない事があります。しかし、意味的にはその項目は必須であり、JSONに書いてないならばデフォルト値を与えて、プロパティの型としては`Optional`でなくしたい事があります。`FineJSON`ではそのようなデフォルト値を簡単に設定する事ができます。

```swift
    struct H : Codable, JSONAnnotatable {
        static let keyAnnotations: JSONKeyAnnotations = [
            "language": JSONKeyAnnotation(defaultValue: JSON.string("Swift"))
        ]
        
        var name: String
        var language: String
    }
    
    func testDefaultValue() throws {
        let json = """
{
  "name": "john"
}
"""
        let d = FineJSONDecoder()
        let h = try d.decode(H.self, from: json.data(using: .utf8)!)
        
        XCTAssertEqual(h.name, "john")
        XCTAssertEqual(h.language, "Swift")
    }
```

## 位置情報の自動デコード

位置情報を自動でCodable対応できます。エンコードした時は消えます。

```swift
    func testAutoLocationDecoding() throws {
        let json = """
// comment
{
  "name": "b"
},
"""
        let decoder = FineJSONDecoder()
        let x = try decoder.decode(C.self, from: json.data(using: .utf8)!)
        XCTAssertEqual(x.location, SourceLocation(offset: 11, line: 2, columnInByte: 1))
        XCTAssertEqual(x.name, "b")
        
        let encoder = FineJSONEncoder()
        let json2 = String(data: try encoder.encode(x), encoding: .utf8)!
        XCTAssertEqual(json2, """
{
  "name": "b"
}
""")
        
    }
```

# サポートしているビルド環境

SwiftPMによるビルドをmac, iOSでサポートしています。

Carthageによるビルドをmac, iOSでサポートしています。

手動でのxcworkspaceによるビルドをmac, iOSでサポートしています。個人的にはCarthageによるバージョン解決と組み合わせて手動ビルドするのがおすすめです。詳しくは[こちらの記事](https://qiita.com/omochimetaru/items/3a8441be9152ea6619b6)で解説しています。

実際には、ビルドはtvOS, watchOSも通していて、テストの実行はtvOSでもできていますが、これらについてはよく知らないのでわかりません。

SwiftPMで通っていて、macなものへの依存は無いはずなのでちょっとやればLinuxでも動く気がしますがまだ試していません。

# 実装

`FineJSON`を実装する上で、`RichJSONParser`という自作のJSONシリアライザと、`OrderedDictionary`という自作の順序付き辞書ライブラリに依存しています。

- https://github.com/omochi/RichJSONParser
- https://github.com/omochi/OrderedDictionary

RichJSONParserは、ケツカンマ、コメント、位置情報などを取り扱うために使っています。

OrderedDictionaryはキーの順序を保持するために使っています。

`Foundation`の`NSJSONSerialization`では、JSONのobjectの内部表現として`NSDictionary`を使います。`NSDictionary`はキーの順序を保持しないため、これはだめです。また、JSONのnumberの内部表現として`NSNumber`を使うため、有限精度しか扱えません。そこで、情報の喪失の無い外部のJSONシリアライザが必要でした。

また、Swiftにも`Foundation`にも順序付き辞書がありません。`Dictionary`と`Array`のペアをラップして簡易に構築する事ができますが、こうすると要素の挿入操作のオーダーが`O(n)`になってしまい、`Dictionary`よりも悪くなってしまいます。これを通常の`Dictionary`と同様に`O(1)`で挿入するためには、辞書と一緒に連結リストを使う必要があります。残念ながら連結リストも提供されていないので、これらをまとめて自作しました。`OrderedDictionary`ライブラリで連結リストの`LinkedList`を実装するにあたって、SwiftらしくCopy on Writeな値型にするために、[涙ぐましい努力](https://github.com/omochi/OrderedDictionary/blob/25c754ff4fd48942dbc43df90234eec8243b44b9/Sources/OrderedDictionary/LinkedList.swift#L56)をしているので興味があったらついでに見てください。