Contents

mattt/swift-yyjson

A fast JSON library for Swift,

Benchmarks

YYJSON delivers significant performance improvements
over Foundation's JSON APIs.
These benchmarks compare parsing times
using standard JSON test fixtures from
[nativejson-benchmark](https://github.com/miloyip/nativejson-benchmark).

| Fixture                      |  YYJSON | Foundation | Speedup |
| :--------------------------- | ------: | ---------: | ------: |
| `twitter.json` (~632KB)      | ~180 μs |    ~2.9 ms |    ~16× |
| `citm_catalog.json` (~1.7MB) | ~425 μs |    ~4.3 ms |    ~10× |
| `canada.json` (~2.2MB)       | ~2.3 ms |   ~36.0 ms |    ~16× |
| `tokenizer.json` (~11MB)     | ~6.5 ms |   ~57.0 ms |     ~9× |

YYJSON also uses significantly less memory.
Parsing twitter.json requires only 3 allocations compared to over 6,600 for Foundation,
with peak memory of 19 MB versus up to 378 MB.
For maximum efficiency,
[in-place parsing](#in-place-parsing)
eliminates allocations entirely by operating directly on the input buffer.

The performance advantage is most pronounced for large files,
access-heavy workloads where YYJSON's value-based API avoids repeated type casting,
and number-heavy data like GeoJSON that benefits from optimized floating-point parsing.

For detailed methodology and additional benchmarks,
see [swift-yyjson-benchmark](https://github.com/mattt/swift-yyjson-benchmark).

<details>

<summary>Raw Results</summary>

```shell
swift package benchmark --format markdown --filter "Fixture/.+/Parse/.+" --time-units microseconds
```

```console
Host 'MacBook-Pro.local' with 16 'arm64' processors with 48 GB memory, running:
Darwin Kernel Version 25.2.0: Tue Nov 18 21:09:56 PST 2025; root:xnu-12377.61.12~1/RELEASE_ARM64_T6041
```

### Fixture/canada.json/Parse/Foundation

| Metric                     |    p0 |   p25 |   p50 |   p75 |   p90 |   p99 |  p100 | Samples |
| :------------------------- | ----: | ----: | ----: | ----: | ----: | ----: | ----: | ------: |
| Instructions (M) \*        |   308 |   308 |   308 |   308 |   309 |   312 |   312 |      85 |
| Malloc (total) (K) \*      |   167 |   167 |   167 |   167 |   167 |   167 |   167 |      85 |
| Memory (resident peak) (M) |    17 |   148 |   274 |   394 |   478 |   524 |   524 |      85 |
| Throughput (# / s) (#)     |    88 |    85 |    85 |    84 |    83 |    82 |    82 |      85 |
| Time (total CPU) (μs) \*   | 11425 | 11731 | 11821 | 11969 | 12034 | 12234 | 12234 |      85 |
| Time (wall clock) (μs) \*  | 11419 | 11723 | 11821 | 11969 | 12034 | 12227 | 12227 |      85 |

### Fixture/canada.json/Parse/YYJSON

| Metric                     |   p0 |  p25 |  p50 |  p75 |  p90 |  p99 | p100 | Samples |
| :------------------------- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ------: |
| Instructions (M) \*        |   35 |   35 |   35 |   35 |   35 |   35 |   35 |     790 |
| Malloc (total) \*          |    3 |    3 |    3 |    3 |    3 |    3 |    3 |     790 |
| Memory (resident peak) (M) |   17 |   22 |   22 |   22 |   22 |   22 |   22 |     790 |
| Throughput (# / s) (#)     |  861 |  810 |  802 |  795 |  787 |  760 |  745 |     790 |
| Time (total CPU) (μs) \*   | 1163 | 1236 | 1249 | 1261 | 1274 | 1318 | 1344 |     790 |
| Time (wall clock) (μs) \*  | 1162 | 1234 | 1247 | 1258 | 1271 | 1316 | 1342 |     790 |

### Fixture/citm_catalog.json/Parse/Foundation

| Metric                     |   p0 |  p25 |  p50 |  p75 |  p90 |  p99 | p100 | Samples |
| :------------------------- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ------: |
| Instructions (M) \*        |   73 |   73 |   73 |   73 |   73 |   73 |   74 |     297 |
| Malloc (total) (K) \*      |   14 |   14 |   14 |   14 |   14 |   14 |   14 |     297 |
| Memory (resident peak) (M) |   18 |   90 |  161 |  230 |  276 |  301 |  301 |     297 |
| Throughput (# / s) (#)     |  312 |  304 |  301 |  296 |  284 |  276 |  273 |     297 |
| Time (total CPU) (μs) \*   | 3205 | 3293 | 3330 | 3383 | 3521 | 3633 | 3660 |     297 |
| Time (wall clock) (μs) \*  | 3203 | 3291 | 3328 | 3381 | 3518 | 3629 | 3659 |     297 |

### Fixture/citm_catalog.json/Parse/YYJSON

| Metric                     |   p0 |  p25 |  p50 |  p75 |  p90 |  p99 |  p100 | Samples |
| :------------------------- | ---: | ---: | ---: | ---: | ---: | ---: | ----: | ------: |
| Instructions (K) \*        | 9850 | 9855 | 9855 | 9855 | 9855 | 9871 | 10528 |    2871 |
| Malloc (total) \*          |    3 |    3 |    3 |    3 |    3 |    3 |     3 |    2871 |
| Memory (resident peak) (M) |   18 |   22 |   22 |   22 |   22 |   22 |    22 |    2871 |
| Throughput (# / s) (#)     | 3253 | 3075 | 3025 | 2973 | 2929 | 2801 |  2590 |    2871 |
| Time (total CPU) (μs) \*   |  309 |  327 |  332 |  338 |  343 |  359 |   392 |    2871 |
| Time (wall clock) (μs) \*  |  307 |  325 |  331 |  336 |  342 |  357 |   386 |    2871 |

### Fixture/twitter.json/Parse/Foundation

| Metric                     |   p0 |  p25 |  p50 |  p75 |  p90 |  p99 | p100 | Samples |
| :------------------------- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ------: |
| Instructions (M) \*        |   44 |   44 |   44 |   44 |   44 |   44 |   44 |     501 |
| Malloc (total) \*          | 6636 | 6637 | 6637 | 6637 | 6637 | 6637 | 6637 |     501 |
| Memory (resident peak) (M) |   18 |  108 |  198 |  285 |  342 |  374 |  378 |     501 |
| Throughput (# / s) (#)     |  531 |  514 |  510 |  505 |  492 |  455 |  436 |     501 |
| Time (total CPU) (μs) \*   | 1887 | 1946 | 1964 | 1985 | 2032 | 2198 | 2296 |     501 |
| Time (wall clock) (μs) \*  | 1883 | 1945 | 1962 | 1982 | 2032 | 2198 | 2294 |     501 |

### Fixture/twitter.json/Parse/YYJSON

| Metric                     |   p0 |  p25 |  p50 |  p75 |  p90 |  p99 | p100 | Samples |
| :------------------------- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ------: |
| Instructions (K) \*        | 3509 | 3510 | 3510 | 3510 | 3510 | 3527 | 3941 |    6785 |
| Malloc (total) \*          |    3 |    3 |    3 |    3 |    3 |    3 |    3 |    6785 |
| Memory (resident peak) (M) |   17 |   19 |   19 |   19 |   19 |   19 |   19 |    6785 |
| Throughput (# / s) (#)     | 8544 | 8179 | 7791 | 7399 | 7267 | 6687 | 2383 |    6785 |
| Time (total CPU) (μs) \*   |  118 |  124 |  130 |  137 |  139 |  152 |  339 |    6785 |
| Time (wall clock) (μs) \*  |  117 |  122 |  128 |  135 |  138 |  150 |  420 |    6785 |

### Fixture/tokenizer.json/Parse/Foundation

| Metric                     |    p0 |   p25 |   p50 |   p75 |   p90 |   p99 |  p100 | Samples |
| :------------------------- | ----: | ----: | ----: | ----: | ----: | ----: | ----: | ------: |
| Instructions (M) \*        |  1212 |  1213 |  1213 |  1213 |  1213 |  1215 |  1215 |      18 |
| Malloc (total) (K) \*      |   382 |   382 |   382 |   382 |   382 |   382 |   382 |      18 |
| Memory (resident peak) (M) |    74 |   158 |   242 |   344 |   407 |   430 |   430 |      18 |
| Throughput (# / s) (#)     |    18 |    18 |    17 |    17 |    17 |    17 |    17 |      18 |
| Time (total CPU) (μs) \*   | 54226 | 56001 | 56984 | 57541 | 58950 | 59070 | 59070 |      18 |
| Time (wall clock) (μs) \*  | 54202 | 56001 | 56951 | 57541 | 58917 | 59050 | 59050 |      18 |

### Fixture/tokenizer.json/Parse/YYJSON

| Metric                     |   p0 |  p25 |  p50 |  p75 |  p90 |  p99 | p100 | Samples |
| :------------------------- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ------: |
| Instructions (M) \*        |  105 |  105 |  105 |  106 |  106 |  106 |  106 |     153 |
| Malloc (total) \*          |    4 |    4 |    4 |    4 |    4 |    4 |    4 |     153 |
| Memory (resident peak) (M) |   28 |   52 |   52 |   52 |   52 |   52 |   52 |     153 |
| Throughput (# / s) (#)     |  158 |  154 |  153 |  153 |  152 |  147 |  127 |     153 |
| Time (total CPU) (μs) \*   | 6316 | 6480 | 6525 | 6562 | 6607 | 6754 | 7863 |     153 |
| Time (wall clock) (μs) \*  | 6315 | 6476 | 6521 | 6554 | 6599 | 6816 | 7857 |     153 |

</details>

Requirements

  • Swift 6.1+ / Xcode 16+
  • macOS 10.15+ / iOS 13+ / tvOS 13+ / watchOS 6+ / visionOS 1+

Installation

Swift Package Manager

Add the following to your Package.swift file:

dependencies: [
    .package(url: "https://github.com/mattt/swift-yyjson.git", from: "0.3.0")
]

Usage

Decoding with Codable

Use YYJSONDecoder as an alternative to JSONDecoder:

import YYJSON

struct User: Codable {
    let id: Int
    let name: String
    let email: String
}

let json = #"{"id": 1, "name": "Alice", "email": "alice@example.com"}"#
let data = json.data(using: .utf8)!

let decoder = YYJSONDecoder()
let user = try decoder.decode(User.self, from: data)
print(user.name) // "Alice"

YYJSONDecoder supports the same decoding strategies as JSONDecoder:

let decoder = YYJSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
decoder.dataDecodingStrategy = .base64

JSON5 Support

Enable JSON5 parsing for more flexible input:

let decoder = YYJSONDecoder()
decoder.allowsJSON5 = true  // Enable all JSON5 features

Or configure individual JSON5 features:

decoder.allowsJSON5 = .init(
    trailingCommas: true,   // Allow [1, 2, 3,]
    comments: true,         // Allow // and /* */ comments
    infAndNaN: true,        // Allow Infinity and NaN literals
    singleQuotedStrings: true  // Allow 'single quotes'
)

[!NOTE] JSON5 support is unavailable when the strictStandardJSON trait is enabled. The allowsJSON5 property and JSON5DecodingOptions type are conditionally compiled and will not be available at compile time.

Encoding with Codable

Use YYJSONEncoder as an alternative to JSONEncoder:

import YYJSON

let user = User(id: 1, name: "Alice", email: "alice@example.com")

let encoder = YYJSONEncoder()
let data = try encoder.encode(user)
print(String(data: data, encoding: .utf8)!)
// {"id":1,"name":"Alice","email":"alice@example.com"}

Configure output formatting:

var encoder = YYJSONEncoder()
encoder.writeOptions = [.prettyPrinted, .escapeUnicode]

YYJSONEncoder supports date encoding strategies:

var encoder = YYJSONEncoder()
encoder.dateEncodingStrategy = .iso8601
// Or: .secondsSince1970, .millisecondsSince1970, .formatted(formatter), .custom(closure)

DOM-Style Access

Parse JSON and access values directly without defining types:

import YYJSON

let json = #"{"users": [{"name": "Alice"}, {"name": "Bob"}]}"#
let value = try YYJSONValue(string: json)

// Access nested values with subscripts
if let name = value["users"]?[0]?["name"]?.string {
    print(name) // "Alice"
}

In-Place Parsing

For maximum performance with large JSON files, use in-place parsing to avoid copying the input data:

var data = try Data(contentsOf: fileURL)
let json = try YYJSONValue.parseInPlace(consuming: &data)
// `data` is now consumed and should not be used

In-place parsing allows yyjson to parse directly within the input buffer, avoiding memory allocation for string storage. The inout parameter makes it clear that the data is consumed by this operation.

[!NOTE] For most use cases, the standard YYJSONValue(data:) initializer is sufficient. Use in-place parsing only when performance is critical and you can accept the ownership semantics.

JSONSerialization Alternative

Use YYJSONSerialization with the same API as Foundation's JSONSerialization:

import YYJSON

let json = #"{"message": "Hello, World!"}"#
let data = json.data(using: .utf8)!

let object = try YYJSONSerialization.jsonObject(with: data)
if let dict = object as? [String: Any] {
    print(dict["message"] as? String ?? "") // "Hello, World!"
}

Configure output formatting with WritingOptions:

// Pretty printing with 2-space indent (useful for Xcode asset catalogs)
let data = try YYJSONSerialization.data(
    withJSONObject: dict,
    options: [.indentationTwoSpaces, .sortedKeys]
)

// ASCII-only output with trailing newline
let data = try YYJSONSerialization.data(
    withJSONObject: dict,
    options: [.escapeUnicode, .newlineAtEnd]
)

Available writing options:

  • .fragmentsAllowed — Allow top-level values that aren't arrays or dictionaries
  • .prettyPrinted — Pretty print with 4-space indent
  • .sortedKeys — Sort dictionary keys lexicographically
  • .withoutEscapingSlashes — Don't escape / as \/
  • .indentationTwoSpaces — Configure pretty printing to use 2-space indent (implies .prettyPrinted)
  • .escapeUnicode — Escape non-ASCII characters as \uXXXX
  • .newlineAtEnd — Add trailing newline \n

Non-standard options (unavailable when strictStandardJSON trait is enabled):

  • .allowInfAndNaN — Write Infinity and NaN literals
  • .infAndNaNAsNull — Write Infinity and NaN as null (takes precedence)

Read and Write Options

Reading Options

Configure parsing behavior with YYJSONReadOptions:

let value = try YYJSONValue(data: data, options: [.allowComments, .allowTrailingCommas])

Available options:

  • .stopWhenDone — Stop after first complete JSON document
  • .numberAsRaw — Read all numbers as raw strings
  • .allowInvalidUnicode — Allow reading invalid unicode
  • .bigNumberAsRaw — Read big numbers as raw strings

Non-standard options (unavailable when strictStandardJSON trait is enabled):

  • .allowTrailingCommas — Allow [1, 2, 3,]
  • .allowComments — Allow // and / / comments
  • .allowInfAndNaN — Allow Infinity, -Infinity, NaN
  • .allowBOM — Allow UTF-8 BOM
  • .allowExtendedNumbers — Allow hex, leading ., trailing ., leading +
  • .allowExtendedEscapes — Allow \a, \e, \v, \xNN, etc.
  • .allowExtendedWhitespace — Allow extended whitespace characters
  • .allowSingleQuotedStrings — Allow 'single quotes'
  • .allowUnquotedKeys — Allow {key: value}
  • .json5 — Enable all JSON5 features

Writing Options

Configure output with YYJSONWriteOptions:

var encoder = YYJSONEncoder()
encoder.writeOptions = [.prettyPrinted, .escapeSlashes]

Available options:

  • .prettyPrinted — Pretty print with 4-space indent
  • .indentationTwoSpaces — Pretty print with 2-space indent (implies .prettyPrinted)
  • .escapeUnicode — Escape non-ASCII as \uXXXX
  • .escapeSlashes — Escape / as \/
  • .allowInvalidUnicode — Allow invalid unicode when encoding
  • .newlineAtEnd — Add trailing newline
  • .sortedKeys — Sort object keys lexicographically

Non-standard options (unavailable when strictStandardJSON trait is enabled):

  • .allowInfAndNaN — Write Infinity and NaN literals
  • .infAndNaNAsNull — Write Infinity and NaN as null (takes precedence)

Package Traits

Customize the underlying yyjson library at compile time using package traits:

.package(
    url: "https://github.com/mattt/swift-yyjson.git",
    from: "0.3.0",
    traits: ["noWriter", "strictStandardJSON"]
)

By default, no traits are enabled — you get full functionality with all features and validations included. Enable traits only when you have specific size or performance requirements.

[!NOTE] When traits are enabled, the corresponding Swift APIs are conditionally compiled and become unavailable at compile time. For example, enabling the noReader trait makes unavailable YYJSONDecoder, YYJSONValue, and YYJSONSerialization.jsonObject(with:options:). Similarly, enabling the noWriter trait makes unavailable YYJSONEncoder and YYJSONSerialization.data(withJSONObject:options:).

noReader

Disables JSON reader functionality at compile-time (functions with "read" in their name). Reduces binary size by about 60%. Use this if your application only needs to write JSON, not parse it.

When this trait is enabled, the following APIs become unavailable:

  • YYJSONDecoder
  • YYJSONValue, YYJSONObject, YYJSONArray
  • YYJSONSerialization.jsonObject(with:options:)

noWriter

Disables JSON writer functionality at compile-time (functions with "write" in their name). Reduces binary size by about 30%. Use this if your application only needs to parse JSON, not generate it.

When this trait is enabled, the following APIs become unavailable:

  • YYJSONEncoder
  • YYJSONSerialization.data(withJSONObject:options:)

noIncrementalReader

Disables the incremental JSON reader at compile-time. Use this if you don't need to parse JSON in streaming/chunked mode.

noUtilities

Disables support for JSON Pointer, JSON Patch, and JSON Merge Patch. Use this if you don't need these utilities for querying or modifying JSON documents.

noFastFloatingPoint

Disables yyjson's fast floating-point number conversion and uses libc's strtod/snprintf instead. Reduces binary size by about 30%, but significantly slows down floating-point read/write speed. Use this only if binary size is critical and you don't process many floating-point values.

strictStandardJSON

Disables non-standard JSON features at compile-time (such as allowing comments, trailing commas, or infinity/NaN values). Reduces binary size by about 10% and slightly improves performance. Use this if you only need to handle strictly conformant JSON.

When this trait is enabled, the following APIs become unavailable:

  • YYJSONReadOptions.allowTrailingCommas
  • YYJSONReadOptions.allowComments
  • YYJSONReadOptions.allowInfAndNaN
  • YYJSONReadOptions.allowBOM
  • YYJSONReadOptions.allowExtendedNumbers
  • YYJSONReadOptions.allowExtendedEscapes
  • YYJSONReadOptions.allowExtendedWhitespace
  • YYJSONReadOptions.allowSingleQuotedStrings
  • YYJSONReadOptions.allowUnquotedKeys
  • YYJSONReadOptions.json5
  • YYJSONWriteOptions.allowInfAndNaN
  • YYJSONWriteOptions.infAndNaNAsNull
  • YYJSONDecoder.allowsJSON5
  • JSON5DecodingOptions
  • YYJSONSerialization.ReadingOptions.json5Allowed

noUTF8Validation

Disables UTF-8 validation at compile-time. Improves performance for non-ASCII strings by about 3% to 7%. Use this only if all input strings are guaranteed to be valid UTF-8.

[!CAUTION] If this trait is enabled while passing invalid UTF-8 data, parsing errors may be silently ignored, strings may merge unexpectedly, or out-of-bounds memory access may occur.

Differences from Foundation

YYJSONDecoder and YYJSONEncoder are designed to be API-compatible with Foundation's JSONDecoder and JSONEncoder for common use cases. However, there are some differences:

  • Error types: Throws YYJSONError instead of DecodingError/EncodingError.

YYJSONSerialization also throws YYJSONError rather than NSError.

  • Encoder strategies: YYJSONEncoder does not yet support

keyEncodingStrategy or nonConformingFloatEncodingStrategy

  • Output formatting: Uses writeOptions instead of outputFormatting
  • Number precision: yyjson parses numbers as 64-bit integers or doubles;

extremely large integers may lose precision

Thread Safety

  • YYJSONDecoder and YYJSONEncoder are value types and safe to use from

multiple threads, as long as each encode/decode call is not shared concurrently.

  • YYJSONValue, YYJSONObject, and YYJSONArray are safe to share across threads

for read-only access; they wrap an immutable yyjson document.

  • The number property on YYJSONValue returns a Double. For exact representation

of very large numbers, parse using .bigNumberAsRaw and read them as strings.

License

This project is available under the MIT license. See the LICENSE file for more info.

The underlying yyjson library is also available under the MIT license.

Package Metadata

Repository: mattt/swift-yyjson

Default branch: main

README: README.md