Skip to content

Implement ReerJSONEncoder using yyjson for high-performance JSON encoding#5

Merged
vnixx merged 7 commits intomainfrom
feature/reer-json-encoder
Apr 14, 2026
Merged

Implement ReerJSONEncoder using yyjson for high-performance JSON encoding#5
vnixx merged 7 commits intomainfrom
feature/reer-json-encoder

Conversation

@vnixx
Copy link
Copy Markdown
Member

@vnixx vnixx commented Apr 14, 2026

Summary

  • Implement ReerJSONEncoder as an API-compatible drop-in replacement for Foundation's JSONEncoder, powered by yyjson for high-performance JSON encoding.
  • Add ReerJSONEncoder public API (ReerJSONEncoder.swift, 270 lines) and internal encoding implementation (JSONEncoderImpl.swift, 545 lines), with full support for all JSONEncoder options (outputFormatting, dateEncodingStrategy, dataEncodingStrategy, keyEncodingStrategy, nonConformingFloatEncodingStrategy, userInfo).
  • Incrementally optimize encoder performance across multiple commits: type dispatch via T.self ==, bulk yyjson array APIs, reuse encoder in wrapGenericEncodable, in-place sort for sorted keys, and refined container-level optimizations.

Differences from Foundation JSONEncoder

Encoder Diff Foundation ReerJSON
Unicode escape casing \u001f (lowercase) \u001F (uppercase). Both are valid JSON per RFC 8259
Pretty-print colon "key" : value (space before and after colon) "key": value (space after colon only)

Changes

  • New: Sources/ReerJSON/ReerJSONEncoder.swift — public encoder class with thread-safe option accessors
  • New: Sources/ReerJSON/JSONEncoderImpl.swift — internal encoder implementation using yyjson C API
  • Updated: Sources/ReerJSON/Utilities.swift — added shared utility helpers
  • Updated: Tests/ReerJSONTests/JSONEncoderTests.swift — adapted test suite for ReerJSONEncoder
  • Updated: README.md — added encoder usage, encoder diff table, marked encoder TODO as done

Test plan

  • All existing encoder tests pass with ReerJSONEncoder substituted for JSONEncoder
  • Verify encoding output matches Foundation behavior for all supported strategies
  • Confirm thread safety of encoder option mutations
  • Benchmark encoding performance vs Foundation JSONEncoder

Made with Cursor

vnixx added 7 commits April 13, 2026 20:54
…ding

- Add ReerJSONEncoder with full API parity to Foundation JSONEncoder
- Implement JSONEncoderImpl using yyjson mutable document API for JSON tree building
- Support all encoding strategies: date, data, key, non-conforming float
- Support outputFormatting: prettyPrinted, sortedKeys, withoutEscapingSlashes
- Support Int128/UInt128 encoding (Swift 6.0+)
- Handle superEncoder, nested containers, redundant key encoding
- All 119 encoder tests passing, 531 total tests passing

Made-with: Cursor
…tive format

- Replace sortMutVal (tree rebuild) with in-place sort via yyjson_mut_obj_clear + re-add
- Remove lowercaseUnicodeEscapes post-processing pass (use yyjson native uppercase hex)
- Remove addSpaceBeforeColonInPrettyJSON post-processing pass (use yyjson native ": " style)
- Update 2 test expectations to match yyjson output format
- Eliminates all extra traversals: now only yyjson builds tree + sorts in-place + serializes once

Made-with: Cursor
…Is, nested double fast paths

- Replace `as?` type checks with `T.self ==` in generic encode<T> contexts (decoder pattern)
- Add wrapGenericEncodable<T> with compile-time type dispatch for all common types
- Use yyjson_mut_arr_with_double/sint/uint bulk C APIs for primitive arrays (zero Swift loop)
- Add fast paths for [[Double]] and [[[Double]]] (Canada geography killer)
- Cover all integer array types: Int8/16/32/64, UInt8/16/32/64, Int, UInt
- All 531 tests passing

Made-with: Cursor
…bject allocation

- Replace sub-encoder creation with _encodeNestedValue() that reuses self
  via push/pop pattern on codingPath + save/restore of singleValue/array/object
- Eliminates ~96.5ns array concat + ~30-50ns class alloc per nested value
- Apply same optimization to wrapDateValue, wrapDataValue, wrapStringKeyedDictValue
- Twitter: 2.26x → 2.53x, Apache: 1.99x → 2.39x, Random: 1.98x → 2.41x (Foundation=1.00x)
- All 119 tests pass
- Remove unnecessary withMemoryRebound in wrapString (Swift auto-bridges UInt8* to Int8* at C boundary)
- Cache doc pointer as let field instead of computed property via impl.doc
- Cache useDefaultKeys flag at init to skip repeated switch on keyEncodingStrategy
- Add _key() fast path that bypasses impl.convertedKey() for default key strategy

Random dataset: 2.39x -> 2.53x (+6%), now matches swift-yyjson
Twitter: 2.52x -> 2.86x (+13%)
Apache: 2.46x -> 2.63x (+7%)
All 119 tests passing.
Remove wrapNestedDoubleArray/wrapTripleNestedDoubleArray special cases.
These were type-specific optimizations that only benefited the Canada
dataset. Multi-dimensional arrays naturally recurse through the encoder
and still hit the [Double] bulk fast path at the leaf level.

Canada: 10.79x -> 3.56x (still 6.5x faster than swift-yyjson's 0.55x)
Other datasets: unaffected
@vnixx vnixx merged commit f7f025c into main Apr 14, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant