swift-extras-json

Swift 5.1 github-actions codecov

此包在 Swift 中提供了一个 json 编码器和解码器(不使用 Foundation 或任何其他依赖项)。该实现符合 RFC8259 规范。与 Linux 上的 Foundation 实现相比,它提供了显著的性能提升。

如果您喜欢在没有任何依赖项的情况下使用 Swift 的想法,您可能也会喜欢我们在 Swift 中重新实现的 Base64:swift-extras-base64

目标

目前不支持

替代方案

用法

swift-extras-json 作为依赖项添加到您的 Package.swift

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

ExtrasJSON 添加到您想要使用它的目标中。

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

像使用 Foundation 编码器和解码器一样使用它。

import ExtrasJSON

let bytesArray  = try XJSONEncoder().encode(myEncodable)
let myDecodable = try XJSONDecoder().decode(MyDecodable.self, from: bytes)

与 SwiftNIO ByteBuffer 一起使用

为了获得最佳性能,请从您的 ByteBuffer 创建一个 [UInt8],即使 buffer.readableBytesView 在技术上也可以工作。

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)

与 Vapor 4 一起使用

通过使用 swift-extras-json 而不是默认的 Foundation 实现,来提高您的 Vapor 4 API 的性能。首先,您需要按照 Vapor 文档 中的描述,实现对 Vapor 的 ContentEncoderContentDecoder 的一致性。

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

接下来,注册编码器和解码器以在 Vapor 中使用

let decoder = XJSONDecoder()
ContentConfiguration.global.use(decoder: decoder, for: .json)

let encoder = XJSONEncoder()
ContentConfiguration.global.use(encoder: encoder, for: .json)

性能

所有测试均在 2019 MacBook Pro (16 英寸 – 2.4 GHz 8 核 Intel Core i9) 上运行。您可以通过克隆此仓库并自行运行测试

# change dir to perf tests
$ cd PerfTests

# compile and run in release mode - IMPORTANT ‼️
$ swift run -c release

编码

macOS Swift 5.1 macOS Swift 5.2 Linux Swift 5.1 Linux Swift 5.2
Foundation 2.61秒 2.62秒 13.03秒 12.52秒
ExtrasJSON 1.23秒 1.25秒 1.13秒 1.05秒
加速 ~2倍 ~2倍 ~10倍 ~10倍

解码

macOS Swift 5.1 macOS Swift 5.2 Linux Swift 5.1 Linux Swift 5.2
Foundation 2.72秒 3.04秒 10.27秒 10.65秒
ExtrasJSON 1.70秒 1.72秒 1.39秒 1.16秒
加速 ~1.5倍 ~1.5倍 ~7倍 ~8倍

变通方法

DateData 呢?

Date 和 Data 是编码和解码的特殊情况。它们确实有默认的、有点特殊的实现

是的,这是默认实现。只有 Apple 知道为什么它不是 ISO 8601Base64。 🙃 由于我不想链接到 Foundation,因此无法像 Foundation 实现那样为 DateData 实现默认的编码和解码策略。因此,如果您想使用默认值以外的其他编码/解码策略,则需要覆盖 encode(to: Encoder)init(from: Decoder)

这可能看起来像这样

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

您可以在 Apple 的文档中找到有关 编码和解码自定义类型 的更多信息。

当然,您可以使用 @propertyWrapper 来使其更优雅

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
}

在测试文件 DateCodingTests 中查看完整示例。

UTF-16 和 UTF-32

如果您的输入是 UTF-16UTF-32 编码的,您可以轻松地将其转换为 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)

贡献

请随时欢迎并鼓励您为 swift-extras-json 做出贡献。这是一个非常年轻的项目,非常欢迎您的帮助。

如果您发现错误、有建议或需要帮助入门,请打开 Issue 或 PR。如果您使用此包,我将感谢您分享您的经验。

当前关注领域

鸣谢