This package provides a json encoder and decoder in Swift (without the use of Foundation or any other dependency).
The implementation is 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
Goals
does not use Foundation at all
does not use unsafe Swift syntax
no external dependencies, only the Swift stdlib required
Use it as you would use the Foundation encoder and decoder.
import ExtrasJSON
let 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.
let result = try XJSONDecoder().decode(
[SampleStructure].self,
from: buffer.readBytes(length: buffer.readableBytes)!)
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.
import Vapor
import ExtrasJSON
extension XJSONEncoder: ContentEncoder {
public func encode<E: Encodable>(
_ 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<D: Decodable>(
_ 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:
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
# 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 and 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:
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
}
}
If your input is UTF-16 or UTF-32 encoded, you can easily convert it to UTF-8:
let utf16 = UInt16[]() // your utf-16 encoded data
let utf8 = Array(String(decoding: utf16, as: Unicode.UTF16.self).utf8)
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
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 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
Goals
unsafe
Swift syntaxCurrently not supported
Data
andDate
CodingKey
s to camelCase or snake_case (I want to look into this)Alternatives
SwiftNIO
.Usage
Add
swift-extras-json
as dependency to yourPackage.swift
:Add
ExtrasJSON
to the target you want to use it in.Use it as you would use the Foundation encoder and decoder.
Use with SwiftNIO ByteBuffer
For maximal performance create an
[UInt8]
from yourByteBuffer
, even thoughbuffer.readableBytesView
would technically work as well.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’sContentEncoder
andContentDecoder
as described in the Vapor docs.Next, register the encoder and decoder for use in Vapor:
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
Encoding
Decoding
Workarounds
What about
Date
andData
?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 as606155818.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 and Base64. 🙃 Since I don’t want to link against Foundation, it is not possible to implement default encoding and decoding strategies for
Date
andData
like the Foundation implementation does. That’s why, if you want to use another encoding/decoding strategy than the default, you need to overwriteencode(to: Encoder)
andinit(from: Decoder)
.This could look like this:
You can find more information about encoding and decoding custom types in Apple’s documentation.
Of course you can use
@propertyWrapper
s to make this more elegant:Checkout a full example in the test file DateCodingTests.
UTF-16 and UTF-32
If your input is UTF-16 or UTF-32 encoded, you can easily convert it to UTF-8:
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:
KeyEncodingStrategy
Credits
@propertyWrappers
and for finding typos.