|
1 |
| -# swift-pure-json |
| 1 | +# pure-swift-json |
2 | 2 |
|
3 | 3 | [](https://swift.org/download/)
|
4 | 4 | [](https://github.com/fabianfett/pure-swift-json/actions)
|
5 | 5 | [](https://codecov.io/gh/fabianfett/pure-swift-json)
|
6 |
| - |
7 |
| - |
8 | 6 |
|
9 |
| -This package provides a json encoder and decoder in pure Swift (without the use of Foundation). |
10 |
| -The implementation shall be [RFC8259](https://tools.ietf.org/html/rfc8259) complient. |
| 7 | +This package provides a json encoder and decoder in pure Swift (without the use of Foundation or any other dependency). |
| 8 | +The implementation is [RFC8259](https://tools.ietf.org/html/rfc8259) complient. It offers a significant performance improvement over the Foundation implementation on Linux. |
11 | 9 |
|
| 10 | +If you like the idea of using pure Swift without any dependencies you might also like my reimplementation of Base64 in pure Swift: [`swift-base64-kit`](https://github.com/fabianfett/swift-base64-kit) |
12 | 11 |
|
13 |
| -### Goals |
| 12 | +‼️ **NOTE:** This is in a very early stage of development. Please take into account that the API might change while in prerelease. |
| 13 | + |
| 14 | +## Goals |
14 | 15 |
|
15 | 16 | - [x] Does not use Foundation at all
|
16 |
| -- [x] Does not use `unsafe` swift syntax |
| 17 | +- [x] Does not use `unsafe` Swift syntax |
17 | 18 | - [x] No external dependencies other than the Swift STL
|
18 | 19 | - [x] Faster than Foundation implementation
|
19 | 20 |
|
20 |
| -### Status |
| 21 | +#### Currently not supported |
21 | 22 |
|
22 |
| -**Note**: Don't use this. This is under heavy development and will be changed a lot! |
| 23 | +- custom encoder and decoder for [ `Data` and `Date`](#what-about-date-and-data) |
| 24 | +- parsing/decoding of [UTF-16 and UTF-32 encoded json](#utf-16-and-utf-32) |
| 25 | +- transform CodingKeys to camelCase or snake_case (I want to look into this) |
23 | 26 |
|
24 |
| -Currently the focus areas are: |
| 27 | +#### Alternatives |
25 | 28 |
|
26 |
| -- This needs **a lot** of tests to ensure correct working and safety against attackers |
27 |
| -- Fix all `preconditionFailure("unimplemented")` |
28 |
| -- Find a way how to allow parsing of `Date` and `Data` |
29 |
| -- Support decoding of other unicode encodings (currently only utf-8 supported) |
| 29 | +- [IkigaJSON](https://github.com/autimatisering/IkigaJSON) Super fast encoding and decoding especially for server side Swift code. Depends on `SwiftNIO`. |
| 30 | +- [Foundation Coding](https://github.com/apple/swift-corelibs-foundation/blob/master/Sources/Foundation/JSONEncoder.swift) |
30 | 31 |
|
31 |
| -### Alternatives |
| 32 | +## Usage |
32 | 33 |
|
33 |
| -- [IkigaJSON](https://github.com/autimatisering/IkigaJSON) |
34 |
| -- [Foundation Coding](https://github.com/apple/swift-corelibs-foundation/blob/master/Sources/Foundation/JSONEncoder.swift) |
| 34 | +Add `pure-swift-json` as dependency to your `Package.swift`: |
| 35 | + |
| 36 | +```swift |
| 37 | + dependencies: [ |
| 38 | + .package(url: "https://github.com/fabianfett/pure-swift-json.git", .upToNextMajore(from: "0.1.0")), |
| 39 | + ], |
| 40 | +``` |
| 41 | + |
| 42 | +Add `PureSwiftJSONCoding` to the target you want to use it in. |
| 43 | + |
| 44 | +```swift |
| 45 | + targets: [ |
| 46 | + .target( |
| 47 | + name: "MyFancyTarget", |
| 48 | + dependencies: ["PureSwiftJSONCoding"]), |
| 49 | + ] |
| 50 | +``` |
| 51 | + |
| 52 | +Use it as you would use the Foundation encoder and decoder. |
| 53 | + |
| 54 | +```swift |
| 55 | +import PureSwiftJSONCoding |
| 56 | + |
| 57 | +let bytesArray = try JSONEncoder().encode(myEncodable) |
| 58 | +let myDecodable = try JSONDecoder().decode(MyDecodable.self, from: bytes) |
| 59 | +``` |
| 60 | + |
| 61 | +## Performance |
| 62 | + |
| 63 | +All tests have been run on a 2019 MacBook Pro (16" – 2,4 GHz 8-Core Intel Core i9). You can run the tests yourself |
| 64 | +by cloning this repo and |
| 65 | + |
| 66 | +```bash |
| 67 | +# change dir to perf tests |
| 68 | +$ cd PerfTests |
| 69 | + |
| 70 | +# compile in release mode - IMPORTANT ‼️ |
| 71 | +$ swift build -c release |
| 72 | + |
| 73 | +# run tests |
| 74 | +$ .build/release/PureSwiftJSONCodingPerfTests |
| 75 | +``` |
| 76 | + |
| 77 | +#### Encoding |
| 78 | + |
| 79 | +| | macOS Swift 5.1 | macOS Swift 5.2 | Linux Swift 5.1 | Linux Swift 5.2 | |
| 80 | +|:--|:--|:--|:--|:--| |
| 81 | +| Foundation | 2.61s | 2.62s | 13.03s | 12.52s | |
| 82 | +| PureSwiftJSON | 1.23s | 1.25s | 1.13s | 1.05s | |
| 83 | +| Speedup | ~2x | ~2x | **~10x** | **~10x** | |
| 84 | + |
| 85 | + |
| 86 | +#### Decoding |
| 87 | + |
| 88 | +| | macOS Swift 5.1 | macOS Swift 5.2 | Linux Swift 5.1 | Linux Swift 5.2 | |
| 89 | +|:--|:--|:--|:--|:--| |
| 90 | +| Foundation | 2.72s | 3.04s | 10.27s | 10.65s | |
| 91 | +| PureSwiftJSON | 1.70s | 1.72s | 1.39s | 1.16s | |
| 92 | +| Speedup | ~1.5x | ~1.5x | **~7x** | **~8x** | |
| 93 | + |
| 94 | +## Workarounds |
| 95 | + |
| 96 | +### What about `Date` and `Data`? |
| 97 | + |
| 98 | +Date and Data are special cases for encoding and decoding. They do have default implementations that are kind off special: |
| 99 | + |
| 100 | +- Date will be encoded as a float |
| 101 | + |
| 102 | + Example: `2020-03-17 16:36:58 +0000` will be encoded as `606155818.503831` |
| 103 | + |
| 104 | +- Data will be encoded as a numeric array. |
| 105 | + |
| 106 | + Example: `0, 1, 2, 3, 255` will be encoded as: `[0, 1, 2, 3, 255]` |
| 107 | + |
| 108 | +Yes that is the default implementation. Only Apple knows why it is not [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) and [Base64](https://en.wikipedia.org/wiki/Base64). 🙃 |
| 109 | +Because I don't want to link against Foundation it is not possible to implement default encoding and decoding strategies for `Date` and `Data` as the Foundation implementation does. That's why, if you want to use another encoding/decoding strategy than the default you need to overwrite `encode(to: Encoder)` and `init(from: Decoder)`. |
| 110 | + |
| 111 | +This could look like this: |
| 112 | + |
| 113 | +```swift |
| 114 | +struct MyEvent: Decodable { |
| 115 | + |
| 116 | + let eventTime: Date |
| 117 | + |
| 118 | + enum CodingKeys: String, CodingKey { |
| 119 | + case eventTime |
| 120 | + } |
| 121 | + |
| 122 | + init(from decoder: Decoder) { |
| 123 | + let container = try decoder.container(keyedBy: CodingKeys.self) |
| 124 | + let dateString = try container.decode(String.self, forKey: .eventTime) |
| 125 | + guard let timestamp = MyEvent.dateFormatter.date(from: dateString) else { |
| 126 | + let dateFormat = String(describing: MyEvent.dateFormatter.dateFormat) |
| 127 | + throw DecodingError.dataCorruptedError(forKey: .eventTime, in: container, debugDescription: |
| 128 | + "Expected date to be in format `\(dateFormat)`, but `\(dateFormat) does not forfill format`") |
| 129 | + } |
| 130 | + self.eventTime = timestamp |
| 131 | + } |
| 132 | + |
| 133 | + private static let dateFormatter: DateFormatter = MyEvent.createDateFormatter() |
| 134 | + private static func createDateFormatter() -> DateFormatter { |
| 135 | + let formatter = DateFormatter() |
| 136 | + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" |
| 137 | + formatter.timeZone = TimeZone(secondsFromGMT: 0) |
| 138 | + formatter.locale = Locale(identifier: "en_US_POSIX") |
| 139 | + return formatter |
| 140 | + } |
| 141 | +} |
| 142 | +``` |
| 143 | + |
| 144 | +You can find more information about [encoding and decoding custom types in Apple's documentation](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types). |
| 145 | + |
| 146 | +### UTF-16 and UTF-32 |
| 147 | + |
| 148 | +If your input is [UTF-16](https://en.wikipedia.org/wiki/UTF-16) or [UTF-32](https://en.wikipedia.org/wiki/UTF-32) encoded, you can easily convert it to UTF-8: |
| 149 | + |
| 150 | +```swift |
| 151 | +let utf16 = UInt16[]() // your utf-16 encoded data |
| 152 | +let utf8 = Array(String(decoding: utf16, as: Unicode.UTF16.self).utf8) |
| 153 | +``` |
| 154 | + |
| 155 | +```swift |
| 156 | +let utf32 = UInt32[]() // your utf-32 encoded data |
| 157 | +let utf8 = Array(String(decoding: utf32, as: Unicode.UTF32.self).utf8) |
| 158 | +``` |
| 159 | + |
| 160 | +## Contributing |
| 161 | + |
| 162 | +Please feel welcome and encouraged to contribute to `pure-swift-json`. This is a very young endeavour and help is always welcome. |
| 163 | + |
| 164 | +If you've found a bug, have a suggestion or need help getting started, please open an Issue or a PR. If you use this package, I'd be grateful for sharing your experience. |
| 165 | + |
| 166 | +Focus areas for the time being: |
| 167 | + |
| 168 | +- ensuring safe use of nested containers while encoding and decoding |
| 169 | +- supporting camelCase and snakeCase aka [`KeyEncodingStrategy`](https://developer.apple.com/documentation/foundation/jsonencoder/keyencodingstrategy) |
| 170 | + |
| 171 | +## Credits |
| 172 | + |
| 173 | +- [@weissi](https://github.com/weissi) thanks for answering all my questions and for opening tickets [SR-12125](https://bugs.swift.org/browse/SR-12125) and [SR-12126](https://bugs.swift.org/browse/SR-12126) |
| 174 | +- [@dinhhungle](https://github.com/dinhhungle) thanks for your quality assurance. It helped a lot! |
| 175 | +- [@Ro-M](https://github.com/Ro-M) thanks for checking my README.md |
0 commit comments