{"id":13872325,"url":"https://github.com/swift-extras/swift-extras-json","last_synced_at":"2025-04-05T16:10:16.211Z","repository":{"id":55051322,"uuid":"242088254","full_name":"swift-extras/swift-extras-json","owner":"swift-extras","description":"JSON encoding and decoding without the use of Foundation in pure Swift.","archived":false,"fork":false,"pushed_at":"2021-01-12T21:34:32.000Z","size":223,"stargazers_count":351,"open_issues_count":11,"forks_count":16,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-03-29T15:10:57.309Z","etag":null,"topics":["json","swift","swift-json","swift-server"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/swift-extras.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-02-21T08:17:15.000Z","updated_at":"2025-03-07T21:50:14.000Z","dependencies_parsed_at":"2022-08-14T10:10:30.213Z","dependency_job_id":null,"html_url":"https://github.com/swift-extras/swift-extras-json","commit_stats":null,"previous_names":["fabianfett/pure-swift-json"],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swift-extras%2Fswift-extras-json","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swift-extras%2Fswift-extras-json/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swift-extras%2Fswift-extras-json/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swift-extras%2Fswift-extras-json/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/swift-extras","download_url":"https://codeload.github.com/swift-extras/swift-extras-json/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247361695,"owners_count":20926643,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["json","swift","swift-json","swift-server"],"created_at":"2024-08-05T23:00:39.903Z","updated_at":"2025-04-05T16:10:16.182Z","avatar_url":"https://github.com/swift-extras.png","language":"Swift","funding_links":[],"categories":["Swift"],"sub_categories":[],"readme":"# swift-extras-json\n\n[![Swift 5.1](https://img.shields.io/badge/Swift-5.1-blue.svg)](https://swift.org/download/)\n[![github-actions](https://github.com/swift-extras/swift-extras-json/workflows/CI/badge.svg)](https://github.com/swift-extras/swift-extras-json/actions)\n[![codecov](https://codecov.io/gh/swift-extras/swift-extras-json/branch/main/graph/badge.svg)](https://codecov.io/gh/swift-extras/swift-extras-json)\n\nThis package provides a json encoder and decoder in Swift (without the use of Foundation or any other dependency). \nThe implementation is [RFC8259](https://tools.ietf.org/html/rfc8259) compliant. It offers a significant performance improvement compared to the Foundation implementation on Linux.\n\nIf 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)\n\n## Goals\n\n- [x] does not use Foundation at all\n- [x] does not use `unsafe` Swift syntax\n- [x] no external dependencies, only the Swift stdlib required\n- [x] faster than Foundation implementation\n\n#### Currently not supported\n\n- custom encoder and decoder for [ `Data` and `Date`](#what-about-date-and-data)\n- parsing/decoding of [UTF-16 and UTF-32 encoded json](#utf-16-and-utf-32)\n- transforming `CodingKey`s to camelCase or snake_case (I want to look into this)\n\n#### Alternatives\n\n- [IkigaJSON](https://github.com/autimatisering/IkigaJSON) super fast encoding and decoding especially for server side Swift code. Depends on `SwiftNIO`.\n- [Foundation Coding](https://github.com/apple/swift-corelibs-foundation/blob/main/Sources/Foundation/JSONEncoder.swift)\n\n## Usage\n\nAdd `swift-extras-json` as dependency to your `Package.swift`:\n\n```swift\n  dependencies: [\n    .package(url: \"https://github.com/swift-extras/swift-extras-json.git\", .upToNextMajor(from: \"0.6.0\")),\n  ],\n```\n\nAdd `ExtrasJSON` to the target you want to use it in.\n\n```swift\n  targets: [\n    .target(name: \"MyFancyTarget\", dependencies: [\n      .product(name: \"ExtrasJSON\", package: \"swift-extras-json\"),\n    ])\n  ]\n```\n\nUse it as you would use the Foundation encoder and decoder.\n\n```swift\nimport ExtrasJSON\n\nlet bytesArray  = try XJSONEncoder().encode(myEncodable)\nlet myDecodable = try XJSONDecoder().decode(MyDecodable.self, from: bytes)\n```\n\n### Use with SwiftNIO ByteBuffer\n\nFor maximal performance create an `[UInt8]` from your `ByteBuffer`, even though `buffer.readableBytesView` would technically work as well.\n\n```swift\nlet result = try XJSONDecoder().decode(\n  [SampleStructure].self,\n  from: buffer.readBytes(length: buffer.readableBytes)!)\n```\n\n```swift\nlet bytes = try XJSONEncoder().encode(encodable)\nvar buffer = byteBufferAllocator.buffer(capacity: bytes.count)\nbuffer.writeBytes(bytes)\n```\n\n\n### Use with Vapor 4\n\nIncrease 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).\n\n```swift\nimport Vapor\nimport ExtrasJSON\n\nextension XJSONEncoder: ContentEncoder {\n  public func encode\u003cE: Encodable\u003e(\n    _ encodable: E,\n    to body: inout ByteBuffer,\n    headers: inout HTTPHeaders) throws\n  {\n    headers.contentType = .json\n    let bytes = try self.encode(encodable)\n    // the buffer's storage is resized in case its capacity is not sufficient\n    body.writeBytes(bytes)\n  }\n}\n\nextension XJSONDecoder: ContentDecoder {\n  public func decode\u003cD: Decodable\u003e(\n    _ decodable: D.Type,\n    from body: ByteBuffer,\n    headers: HTTPHeaders) throws -\u003e D\n  {\n    guard headers.contentType == .json || headers.contentType == .jsonAPI else {\n      throw Abort(.unsupportedMediaType)\n    }\n    var body = body\n    return try self.decode(D.self, from: body.readBytes(length: body.readableBytes)!)\n  }\n}\n```\n\nNext, register the encoder and decoder for use in Vapor:\n\n```swift\nlet decoder = XJSONDecoder()\nContentConfiguration.global.use(decoder: decoder, for: .json)\n\nlet encoder = XJSONEncoder()\nContentConfiguration.global.use(encoder: encoder, for: .json)\n```\n\n\n## Performance\n\nAll tests have been run on a 2019 MacBook Pro (16\" – 2,4 GHz 8-Core Intel Core i9). You can run the tests yourself\nby cloning this repo and\n\n```bash\n# change dir to perf tests\n$ cd PerfTests\n\n# compile and run in release mode - IMPORTANT ‼️\n$ swift run -c release\n```\n\n#### Encoding\n\n|  | macOS Swift 5.1 | macOS Swift 5.2 | Linux Swift 5.1 | Linux Swift 5.2 |\n|:--|:--|:--|:--|:--|\n| Foundation   | 2.61s | 2.62s | 13.03s | 12.52s |\n| ExtrasJSON | 1.23s | 1.25s | 1.13s | 1.05s |\n| Speedup | ~2x | ~2x | **~10x** | **~10x** |\n\n\n#### Decoding\n\n|  | macOS Swift 5.1 | macOS Swift 5.2 | Linux Swift 5.1 | Linux Swift 5.2 |\n|:--|:--|:--|:--|:--|\n| Foundation   | 2.72s | 3.04s | 10.27s | 10.65s |\n| ExtrasJSON | 1.70s | 1.72s | 1.39s | 1.16s |\n| Speedup | ~1.5x | ~1.5x | **~7x** | **~8x** |\n\n## Workarounds\n\n### What about `Date` and `Data`?\n\nDate and Data are particular cases for encoding and decoding. They do have default implementations that are kind off special:\n\n- Date will be encoded as a float\n\n    Example: `2020-03-17 16:36:58 +0000` will be encoded as `606155818.503831`\n    \n- Data will be encoded as a numeric array.\n\n    Example: `0, 1, 2, 3, 255` will be encoded as: `[0, 1, 2, 3, 255]`\n    \nYes, 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). 🙃\nSince 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)`.\n\nThis could look like this:\n\n```swift\nstruct MyEvent: Decodable {\n\n  let eventTime: Date\n  \n  enum CodingKeys: String, CodingKey {\n    case eventTime\n  }\n\n  init(from decoder: Decoder) {\n    let container = try decoder.container(keyedBy: CodingKeys.self)\n    let dateString = try container.decode(String.self, forKey: .eventTime)\n    guard let timestamp = MyEvent.dateFormatter.date(from: dateString) else {\n      let dateFormat = String(describing: MyEvent.dateFormatter.dateFormat)\n      throw DecodingError.dataCorruptedError(forKey: .eventTime, in: container, debugDescription:\n        \"Expected date to be in format `\\(dateFormat)`, but `\\(dateString) does not fulfill format`\")\n    }\n    self.eventTime = timestamp\n  }\n  \n  private static let dateFormatter: DateFormatter = MyEvent.createDateFormatter()\n  private static func createDateFormatter() -\u003e DateFormatter {\n    let formatter = DateFormatter()\n    formatter.dateFormat = \"yyyy-MM-dd'T'HH:mm:ss.SSSZ\"\n    formatter.timeZone   = TimeZone(secondsFromGMT: 0)\n    formatter.locale     = Locale(identifier: \"en_US_POSIX\")\n    return formatter\n  }\n}\n```\n\nYou 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).\n\nOf course you can use `@propertyWrapper`s to make this more elegant:\n\n```swift\nimport Foundation\n\n@propertyWrapper\nstruct DateStringCoding: Decodable {\n  var wrappedValue: Date\n  \n  init(wrappedValue: Date) {\n    self.wrappedValue = wrappedValue\n  }\n\n  init(from decoder: Decoder) throws {\n    let container = try decoder.singleValueContainer()\n    let dateString = try container.decode(String.self)\n    guard let date = Self.dateFormatter.date(from: dateString) else {\n      let dateFormat = String(describing: Self.dateFormatter.dateFormat)\n      throw DecodingError.dataCorruptedError(in: container, debugDescription:\n            \"Expected date to be in format `\\(dateFormat)`, but `\\(dateString) does not fulfill format`\")\n    }\n    self.wrappedValue = date\n  }\n\n  private static let dateFormatter: DateFormatter = Self.createDateFormatter()\n  private static func createDateFormatter() -\u003e DateFormatter {\n    let formatter = DateFormatter()\n    formatter.dateFormat = \"yyyy-MM-dd'T'HH:mm:ss.SSSZ\"\n    formatter.timeZone   = TimeZone(secondsFromGMT: 0)\n    formatter.locale     = Locale(identifier: \"en_US_POSIX\")\n    return formatter\n  }\n}\n\nstruct MyEvent: Decodable {\n  @DateStringCoding\n  var eventTime: Date\n}\n```\n\nCheckout a full example in the test file [DateCodingTests](https://github.com/swift-extras/swift-extras-json/blob/main/Tests/ExtrasJSONTests/DateCodingTests.swift).\n\n### UTF-16 and UTF-32\n\nIf 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: \n\n```swift\nlet utf16 = UInt16[]() // your utf-16 encoded data\nlet utf8  = Array(String(decoding: utf16, as: Unicode.UTF16.self).utf8)\n```\n\n```swift\nlet utf32 = UInt32[]() // your utf-32 encoded data\nlet utf8  = Array(String(decoding: utf32, as: Unicode.UTF32.self).utf8)\n```\n\n## Contributing\n\nPlease feel welcome and encouraged to contribute to `swift-extras-json`. This is a very young endeavour and help is always welcome.\n\nIf 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.\n\nFocus areas for the time being:\n\n- ensuring safe use of nested containers while encoding and decoding\n- supporting camelCase and snakeCase aka [`KeyEncodingStrategy`](https://developer.apple.com/documentation/foundation/jsonencoder/keyencodingstrategy)\n\n## Credits\n\n- [@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)\n- [@dinhhungle](https://github.com/dinhhungle) thanks for your quality assurance. It helped a lot! \n- [@Ro-M](https://github.com/Ro-M) thanks for checking my README.md\n- [@Trzyipolkostkicukru](https://github.com/Trzyipolkostkicukru) thanks for your advice on `@propertyWrappers` and for finding typos.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fswift-extras%2Fswift-extras-json","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fswift-extras%2Fswift-extras-json","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fswift-extras%2Fswift-extras-json/lists"}