YYJSON

February 6, 2026 · View on GitHub

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.

Benchmarks

YYJSON delivers significant performance improvements over Foundation's JSON APIs. These benchmarks compare parsing times using standard JSON test fixtures from nativejson-benchmark.

FixtureYYJSONFoundationSpeedup
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 microseconds
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

Metricp0p25p50p75p90p99p100Samples
Instructions (M) *30830830830830931231285
Malloc (total) (K) *16716716716716716716785
Memory (resident peak) (M)1714827439447852452485
Throughput (# / s) (#)8885858483828285
Time (total CPU) (μs) *1142511731118211196912034122341223485
Time (wall clock) (μs) *1141911723118211196912034122271222785

Fixture/canada.json/Parse/YYJSON

Metricp0p25p50p75p90p99p100Samples
Instructions (M) *35353535353535790
Malloc (total) *3333333790
Memory (resident peak) (M)17222222222222790
Throughput (# / s) (#)861810802795787760745790
Time (total CPU) (μs) *1163123612491261127413181344790
Time (wall clock) (μs) *1162123412471258127113161342790

Fixture/citm_catalog.json/Parse/Foundation

Metricp0p25p50p75p90p99p100Samples
Instructions (M) *73737373737374297
Malloc (total) (K) *14141414141414297
Memory (resident peak) (M)1890161230276301301297
Throughput (# / s) (#)312304301296284276273297
Time (total CPU) (μs) *3205329333303383352136333660297
Time (wall clock) (μs) *3203329133283381351836293659297

Fixture/citm_catalog.json/Parse/YYJSON

Metricp0p25p50p75p90p99p100Samples
Instructions (K) *985098559855985598559871105282871
Malloc (total) *33333332871
Memory (resident peak) (M)182222222222222871
Throughput (# / s) (#)32533075302529732929280125902871
Time (total CPU) (μs) *3093273323383433593922871
Time (wall clock) (μs) *3073253313363423573862871

Fixture/twitter.json/Parse/Foundation

Metricp0p25p50p75p90p99p100Samples
Instructions (M) *44444444444444501
Malloc (total) *6636663766376637663766376637501
Memory (resident peak) (M)18108198285342374378501
Throughput (# / s) (#)531514510505492455436501
Time (total CPU) (μs) *1887194619641985203221982296501
Time (wall clock) (μs) *1883194519621982203221982294501

Fixture/twitter.json/Parse/YYJSON

Metricp0p25p50p75p90p99p100Samples
Instructions (K) *35093510351035103510352739416785
Malloc (total) *33333336785
Memory (resident peak) (M)171919191919196785
Throughput (# / s) (#)85448179779173997267668723836785
Time (total CPU) (μs) *1181241301371391523396785
Time (wall clock) (μs) *1171221281351381504206785

Fixture/tokenizer.json/Parse/Foundation

Metricp0p25p50p75p90p99p100Samples
Instructions (M) *121212131213121312131215121518
Malloc (total) (K) *38238238238238238238218
Memory (resident peak) (M)7415824234440743043018
Throughput (# / s) (#)1818171717171718
Time (total CPU) (μs) *5422656001569845754158950590705907018
Time (wall clock) (μs) *5420256001569515754158917590505905018

Fixture/tokenizer.json/Parse/YYJSON

Metricp0p25p50p75p90p99p100Samples
Instructions (M) *105105105106106106106153
Malloc (total) *4444444153
Memory (resident peak) (M)28525252525252153
Throughput (# / s) (#)158154153153152147127153
Time (total CPU) (μs) *6316648065256562660767547863153
Time (wall clock) (μs) *6315647665216554659968167857153

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.