Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/swift-extras/swift-extras-json
JSON encoding and decoding without the use of Foundation in pure Swift.
https://github.com/swift-extras/swift-extras-json
json swift swift-json swift-server
Last synced: 18 days ago
JSON representation
JSON encoding and decoding without the use of Foundation in pure Swift.
- Host: GitHub
- URL: https://github.com/swift-extras/swift-extras-json
- Owner: swift-extras
- License: apache-2.0
- Created: 2020-02-21T08:17:15.000Z (over 4 years ago)
- Default Branch: main
- Last Pushed: 2021-01-12T21:34:32.000Z (almost 4 years ago)
- Last Synced: 2024-10-14T22:20:40.306Z (30 days ago)
- Topics: json, swift, swift-json, swift-server
- Language: Swift
- Homepage:
- Size: 218 KB
- Stars: 348
- Watchers: 6
- Forks: 15
- Open Issues: 10
-
Metadata Files:
- Readme: README.md
- License: LICENSE.txt
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
# swift-extras-json
[![Swift 5.1](https://img.shields.io/badge/Swift-5.1-blue.svg)](https://swift.org/download/)
[![github-actions](https://github.com/swift-extras/swift-extras-json/workflows/CI/badge.svg)](https://github.com/swift-extras/swift-extras-json/actions)
[![codecov](https://codecov.io/gh/swift-extras/swift-extras-json/branch/main/graph/badge.svg)](https://codecov.io/gh/swift-extras/swift-extras-json)This package provides a json encoder and decoder in Swift (without the use of Foundation or any other dependency).
The implementation is [RFC8259](https://tools.ietf.org/html/rfc8259) compliant. It offers a significant performance improvement compared to the Foundation implementation on Linux.If you like the idea of using Swift without any dependencies, you might also like our reimplementation of Base64 in Swift: [`swift-extras-base64`](https://github.com/swift-extras/swift-extras-base64)
## Goals
- [x] does not use Foundation at all
- [x] does not use `unsafe` Swift syntax
- [x] no external dependencies, only the Swift stdlib required
- [x] faster than Foundation implementation#### Currently not supported
- custom encoder and decoder for [ `Data` and `Date`](#what-about-date-and-data)
- parsing/decoding of [UTF-16 and UTF-32 encoded json](#utf-16-and-utf-32)
- transforming `CodingKey`s to camelCase or snake_case (I want to look into this)#### Alternatives
- [IkigaJSON](https://github.com/autimatisering/IkigaJSON) super fast encoding and decoding especially for server side Swift code. Depends on `SwiftNIO`.
- [Foundation Coding](https://github.com/apple/swift-corelibs-foundation/blob/main/Sources/Foundation/JSONEncoder.swift)## Usage
Add `swift-extras-json` as dependency to your `Package.swift`:
```swift
dependencies: [
.package(url: "https://github.com/swift-extras/swift-extras-json.git", .upToNextMajor(from: "0.6.0")),
],
```Add `ExtrasJSON` to the target you want to use it in.
```swift
targets: [
.target(name: "MyFancyTarget", dependencies: [
.product(name: "ExtrasJSON", package: "swift-extras-json"),
])
]
```Use it as you would use the Foundation encoder and decoder.
```swift
import ExtrasJSONlet bytesArray = try XJSONEncoder().encode(myEncodable)
let myDecodable = try XJSONDecoder().decode(MyDecodable.self, from: bytes)
```### Use with SwiftNIO ByteBuffer
For maximal performance create an `[UInt8]` from your `ByteBuffer`, even though `buffer.readableBytesView` would technically work as well.
```swift
let result = try XJSONDecoder().decode(
[SampleStructure].self,
from: buffer.readBytes(length: buffer.readableBytes)!)
``````swift
let bytes = try XJSONEncoder().encode(encodable)
var buffer = byteBufferAllocator.buffer(capacity: bytes.count)
buffer.writeBytes(bytes)
```### Use with Vapor 4
Increase the performance of your Vapor 4 API by using `swift-extras-json` instead of the default Foundation implementation. First you'll need to implement the conformance to Vapor's `ContentEncoder` and `ContentDecoder` as described in the [Vapor docs](https://docs.vapor.codes/4.0/content/#custom-coders).
```swift
import Vapor
import ExtrasJSONextension XJSONEncoder: ContentEncoder {
public func encode(
_ encodable: E,
to body: inout ByteBuffer,
headers: inout HTTPHeaders) throws
{
headers.contentType = .json
let bytes = try self.encode(encodable)
// the buffer's storage is resized in case its capacity is not sufficient
body.writeBytes(bytes)
}
}extension XJSONDecoder: ContentDecoder {
public func decode(
_ decodable: D.Type,
from body: ByteBuffer,
headers: HTTPHeaders) throws -> D
{
guard headers.contentType == .json || headers.contentType == .jsonAPI else {
throw Abort(.unsupportedMediaType)
}
var body = body
return try self.decode(D.self, from: body.readBytes(length: body.readableBytes)!)
}
}
```Next, register the encoder and decoder for use in Vapor:
```swift
let decoder = XJSONDecoder()
ContentConfiguration.global.use(decoder: decoder, for: .json)let encoder = XJSONEncoder()
ContentConfiguration.global.use(encoder: encoder, for: .json)
```## Performance
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
by cloning this repo and```bash
# change dir to perf tests
$ cd PerfTests# compile and run in release mode - IMPORTANT ‼️
$ swift run -c release
```#### Encoding
| | macOS Swift 5.1 | macOS Swift 5.2 | Linux Swift 5.1 | Linux Swift 5.2 |
|:--|:--|:--|:--|:--|
| Foundation | 2.61s | 2.62s | 13.03s | 12.52s |
| ExtrasJSON | 1.23s | 1.25s | 1.13s | 1.05s |
| Speedup | ~2x | ~2x | **~10x** | **~10x** |#### Decoding
| | macOS Swift 5.1 | macOS Swift 5.2 | Linux Swift 5.1 | Linux Swift 5.2 |
|:--|:--|:--|:--|:--|
| Foundation | 2.72s | 3.04s | 10.27s | 10.65s |
| ExtrasJSON | 1.70s | 1.72s | 1.39s | 1.16s |
| Speedup | ~1.5x | ~1.5x | **~7x** | **~8x** |## Workarounds
### What about `Date` and `Data`?
Date and Data are particular cases for encoding and decoding. They do have default implementations that are kind off special:
- Date will be encoded as a float
Example: `2020-03-17 16:36:58 +0000` will be encoded as `606155818.503831`
- Data will be encoded as a numeric array.Example: `0, 1, 2, 3, 255` will be encoded as: `[0, 1, 2, 3, 255]`
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). 🙃
Since I don't want to link against Foundation, it is not possible to implement default encoding and decoding strategies for `Date` and `Data` like 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)`.This could look like this:
```swift
struct MyEvent: Decodable {let eventTime: Date
enum CodingKeys: String, CodingKey {
case eventTime
}init(from decoder: Decoder) {
let container = try decoder.container(keyedBy: CodingKeys.self)
let dateString = try container.decode(String.self, forKey: .eventTime)
guard let timestamp = MyEvent.dateFormatter.date(from: dateString) else {
let dateFormat = String(describing: MyEvent.dateFormatter.dateFormat)
throw DecodingError.dataCorruptedError(forKey: .eventTime, in: container, debugDescription:
"Expected date to be in format `\(dateFormat)`, but `\(dateString) does not fulfill format`")
}
self.eventTime = timestamp
}
private static let dateFormatter: DateFormatter = MyEvent.createDateFormatter()
private static func createDateFormatter() -> DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter
}
}
```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).
Of course you can use `@propertyWrapper`s to make this more elegant:
```swift
import Foundation@propertyWrapper
struct DateStringCoding: Decodable {
var wrappedValue: Date
init(wrappedValue: Date) {
self.wrappedValue = wrappedValue
}init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
guard let date = Self.dateFormatter.date(from: dateString) else {
let dateFormat = String(describing: Self.dateFormatter.dateFormat)
throw DecodingError.dataCorruptedError(in: container, debugDescription:
"Expected date to be in format `\(dateFormat)`, but `\(dateString) does not fulfill format`")
}
self.wrappedValue = date
}private static let dateFormatter: DateFormatter = Self.createDateFormatter()
private static func createDateFormatter() -> DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter
}
}struct MyEvent: Decodable {
@DateStringCoding
var eventTime: Date
}
```Checkout a full example in the test file [DateCodingTests](https://github.com/swift-extras/swift-extras-json/blob/main/Tests/ExtrasJSONTests/DateCodingTests.swift).
### UTF-16 and UTF-32
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:
```swift
let utf16 = UInt16[]() // your utf-16 encoded data
let utf8 = Array(String(decoding: utf16, as: Unicode.UTF16.self).utf8)
``````swift
let utf32 = UInt32[]() // your utf-32 encoded data
let utf8 = Array(String(decoding: utf32, as: Unicode.UTF32.self).utf8)
```## Contributing
Please feel welcome and encouraged to contribute to `swift-extras-json`. This is a very young endeavour and help is always welcome.
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.
Focus areas for the time being:
- ensuring safe use of nested containers while encoding and decoding
- supporting camelCase and snakeCase aka [`KeyEncodingStrategy`](https://developer.apple.com/documentation/foundation/jsonencoder/keyencodingstrategy)## Credits
- [@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)
- [@dinhhungle](https://github.com/dinhhungle) thanks for your quality assurance. It helped a lot!
- [@Ro-M](https://github.com/Ro-M) thanks for checking my README.md
- [@Trzyipolkostkicukru](https://github.com/Trzyipolkostkicukru) thanks for your advice on `@propertyWrappers` and for finding typos.