Swiftpack.co - Package - fabianfett/pure-swift-json

pure-swift-json

Swift 5.1 github-actions codecov

This package provides a json encoder and decoder in pure 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 pure Swift without any dependencies, you might also like my reimplementation of Base64 in pure Swift: swift-base64-kit

Goals

  • ☑ does not use Foundation at all
  • ☑ does not use unsafe Swift syntax
  • ☑ no external dependencies other than the Swift STL
  • ☑ faster than Foundation implementation

Currently not supported

Alternatives

Usage

Add pure-swift-json as dependency to your Package.swift:

  dependencies: [
    .package(url: "https://github.com/fabianfett/pure-swift-json.git", .upToNextMajor(from: "0.2.1")),
  ],

Add PureSwiftJSON to the target you want to use it in.

  targets: [
    .target(name: "MyFancyTarget", dependencies: [
      .product(name: "PureSwiftJSON", package: "pure-swift-json"),
    ])
  ]

Use it as you would use the Foundation encoder and decoder.

import PureSwiftJSON

let bytesArray  = try PSJSONEncoder().encode(myEncodable)
let myDecodable = try PSJSONDecoder().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 pureDecoder.decode(
  [SampleStructure].self,
  from: buffer.readBytes(length: buffer.readableBytes)!)
let bytes = try pureEncoder.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 pure-swift-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 PureSwiftJSON

extension PSJSONEncoder: 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 PSJSONDecoder: 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 = PSJSONDecoder()
ContentConfiguration.global.use(decoder: decoder, for: .json)

let encoder = PSJSONEncoder()
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 | | PureSwiftJSON | 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 | | PureSwiftJSON | 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
  }
}

You can find more information about encoding and decoding custom types in Apple's documentation.

Of course you can use @propertyWrappers to make this more elegant:

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.

UTF-16 and UTF-32

If your input is UTF-16 or UTF-32 encoded, you can easily convert it to UTF-8:

let utf16 = UInt16[](https://raw.github.com/fabianfett/pure-swift-json/blob/main/) // your utf-16 encoded data
let utf8  = Array(String(decoding: utf16, as: Unicode.UTF16.self).utf8)
let utf32 = UInt32[](https://raw.github.com/fabianfett/pure-swift-json/blob/main/) // 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 pure-swift-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

Credits

Github

link
Stars: 276

Dependencies

Used By

Total: 0

Releases

pure-swift-json v0.4.0 - 2020-07-12 21:08:03

  • fixes an issue that led to a crash, if an empty array was decoded (#52)
  • makes PSJSONEncoder's and PSJSONDecoder's userInfo public (Added to public api) (#50 and #51)

Thanks to @ktoso and @calebkleveter for reporting the issues fixed in this release!

pure-swift-json v0.3.0 - 2020-06-24 13:18:38

⚠️ This release includes breaking api changes

  • JSONEncoder and JSONDecoder are now prefixed with PS to remove naming conflicts with Foundation. (#48)
  • PSJSONEncoder is now a struct (#47)
  • PSJSONEncoder can now encode into JSONValue and PSJSONDecoder can decode from JSONValue (#46)
  • JSONValue.debugDataTypeDescription is now internal (#29 #45)
  • everything is now under one target PureSwiftJSON. (#44)
  • The code is now formatted with swiftformat. This is enforced with CI (#43)
  • The "early stage development" warning has been removed (#49)

This shall be one of the last releases before 1.0.0. With the changes in the project structure and the renaming of the PSJSONEncoder and PSJSONDecoder, I hope that the coming changes will not be source breaking.

pure-swift-json v0.2.3 - 2020-06-24 07:14:36

  • Fixes a bug where only uppercase unicode escape sequences were allowed. Before only \u003C was supported, now we support \u003c as well (#42)

pure-swift-json v0.2.2 - 2020-06-11 08:40:18

  • Fixes a bug in JSONUnkeyedDecodingContainer (#41)

pure-swift-json v0.2.1 - 2020-04-01 07:02:50

  • Decoding an object from an unknown key within an object doesn't crash anymore.

pure-swift-json v0.2.0 - 2020-03-20 15:46:03

Fixed a couple of issues.

  • Fixed a couple of typos in the README.md (thx for reporting @Trzyipolkostkicukru) #23
  • Fixed typo decodeFixedWithInteger() -> decodeFixedWidthInteger() (thx @rjchatfield) #22 #18
  • Updated the README.md with an example of how to use Date with a propertyWrapper (thx @Trzyipolkostkicukru) #17
  • Allow encode<T: Encodable>(_ value: T) in JSONSingleValueEncodingContainer (thx @adam-fowler) #20 #24
  • Added a License (Apache 2.0) #27
  • JSONEncoder nestedContainer() and nestedUnkeyedContainer() work #19 #25 (thx for reporting @adam-fowler)
  • JSONEncoder updates coding path #21 #25 (thx for reporting @adam-fowler)
  • If NaN or Infinity is encoded an error is thrown #13 (thx for reporting @dinhhungle)

pure-swift-json v0.1.0 - 2020-03-18 10:25:34

The very first release! Feedback highly welcome.