A fast JSON library for Swift, powered by yyjson.
YYJSON provides API-compatible alternatives for
JSONEncoder, JSONDecoder, and JSONSerialization.
It supports configurable compile-time options via
Swift package traits
for further optimization.
YYJSON delivers significant performance improvements over Foundation's JSON APIs. These benchmarks compare parsing times using standard JSON test fixtures from 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 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.
Raw Results
swift package benchmark --format markdown --filter "Fixture/.+/Parse/.+" --time-units microsecondsHost '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| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
- Swift 6.1+ / Xcode 16+
- macOS 10.15+ / iOS 13+ / tvOS 13+ / watchOS 6+ / visionOS 1+
Add the following to your Package.swift file:
dependencies: [
.package(url: "https://github.com/mattt/swift-yyjson.git", from: "0.3.0")
]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 = .base64Enable JSON5 parsing for more flexible input:
let decoder = YYJSONDecoder()
decoder.allowsJSON5 = true // Enable all JSON5 featuresOr 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.
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)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"
}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 usedIn-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.
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— WriteInfinityandNaNliterals.infAndNaNAsNull— WriteInfinityandNaNasnull(takes precedence)
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— AllowInfinity,-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
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— WriteInfinityandNaNliterals.infAndNaNAsNull— WriteInfinityandNaNasnull(takes precedence)
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:).
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:
YYJSONDecoderYYJSONValue,YYJSONObject,YYJSONArrayYYJSONSerialization.jsonObject(with:options:)
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:
YYJSONEncoderYYJSONSerialization.data(withJSONObject:options:)
Disables the incremental JSON reader at compile-time. Use this if you don't need to parse JSON in streaming/chunked mode.
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.
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.
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.allowTrailingCommasYYJSONReadOptions.allowCommentsYYJSONReadOptions.allowInfAndNaNYYJSONReadOptions.allowBOMYYJSONReadOptions.allowExtendedNumbersYYJSONReadOptions.allowExtendedEscapesYYJSONReadOptions.allowExtendedWhitespaceYYJSONReadOptions.allowSingleQuotedStringsYYJSONReadOptions.allowUnquotedKeysYYJSONReadOptions.json5YYJSONWriteOptions.allowInfAndNaNYYJSONWriteOptions.infAndNaNAsNullYYJSONDecoder.allowsJSON5JSON5DecodingOptionsYYJSONSerialization.ReadingOptions.json5Allowed
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.
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
YYJSONErrorinstead ofDecodingError/EncodingError.YYJSONSerializationalso throwsYYJSONErrorrather thanNSError. -
Encoder strategies:
YYJSONEncoderdoes not yet supportkeyEncodingStrategyornonConformingFloatEncodingStrategy -
Output formatting: Uses
writeOptionsinstead ofoutputFormatting -
Number precision: yyjson parses numbers as 64-bit integers or doubles; extremely large integers may lose precision
YYJSONDecoderandYYJSONEncoderare value types and safe to use from multiple threads, as long as eachencode/decodecall is not shared concurrently.YYJSONValue,YYJSONObject, andYYJSONArrayare safe to share across threads for read-only access; they wrap an immutable yyjson document.- The
numberproperty onYYJSONValuereturns aDouble. For exact representation of very large numbers, parse using.bigNumberAsRawand read them as strings.
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.