FineJSON

FineJSON 提供了 FineJSONEncoderFineJSONDecoder,它们是比 Codable 更实用的编码器。它们替代了标准 FoundationJSONEncoderJSONDecoder。这个库旨在帮助解决现实世界中一些奇怪的实际需求。

索引

功能

本节中所有示例代码的工作代码都在 FeaturesTests 中。

允许不必要的末尾逗号

解码器允许不必要的末尾逗号。

    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)])
    }

允许注释

解码器允许 JSON 中的注释。

    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 中的位置。行号、列号(以字节偏移量表示)、字节偏移量。

    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)"))
        }
    }

文件路径也可以传递给解码器。这提升了调试体验。

    func testSourceLocationFilePath() throws {
        let json = """
{ invalid syntax }
"""
        do {
            let decoder = FineJSONDecoder()
            decoder.file = URL(fileURLWithPath: "resource/dir/info.json")
            _ = try decoder.decode(Int.self, from: json.data(using: .utf8)!)
            XCTFail("expect throw")
        } catch {
            let message = "\(error)"
            XCTAssertTrue(message.contains("resource/dir/info.json"))
        }
    }

来自解码器的位置信息

您可以从 Decoder 获取位置信息。

    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 键顺序

编码器保留 JSON 键顺序。

    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)
    }

Foundation.JSONEncoder 没有定义键顺序。所以您可能会遇到这种情况。

{
  "d": "d",
  "b": "b",
  "c": 2,
  "a": 1
}

控制 Optional.none 编码

您可以指定 Optional.none 编码。

默认是键缺失,这与 Foundation 相同。

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

您可以指定为此类键发出显式的 null。

    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)
    }

控制缩进宽度

您可以指定缩进宽度。

    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)
    }

也支持单行样式。

    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)
    }

漂亮打印是默认设置。

处理任意位数的数字

JSON 最初支持任意位数的数字。您可以使用 JSONNumber 类型来处理这种情况。

    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)
    }

Foundation.JSONEncoder 无法做到这一点。所以您可能会在使用 Float 时遇到这种情况。

{
  "x": 1234567936,
  "y": 0.019999999552965164
}

弱类型原始解码

JSON 数字和字符串在解码期间彼此兼容。

    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")
    }

您可以通过注入符合 CodablePrimitiveJSONDecoder 的对象来定制此行为。

直接处理复杂的 JSON 结构

您可以使用 JSON 类型来处理复杂的结构。

    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 键,同时保持 Codable 方法的自动合成。

    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 键不存在时使用。

    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")
    }

自动位置信息解码

可以通过注解启用位置信息解码。

    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"
}
""")
        
    }

支持的构建环境

注意事项

URL, Date 的编码

此库将 URL 序列化为 JSON 中的对象,而不是字符串。这与 Foundation.JSONEncoder、`.JSONDecoder` 不同。因为此库对这些类型使用原生编码定义。Foundation 编码器通过遵循其内部自定义编码逻辑将它们序列化为字符串。

许可证

MIT。