From fb2758020873031f527495d7bdf2d1306d0011e1 Mon Sep 17 00:00:00 2001 From: phoenix Date: Mon, 13 Apr 2026 20:54:48 +0800 Subject: [PATCH 1/7] Implement ReerJSONEncoder using yyjson for high-performance JSON encoding - 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 --- Sources/ReerJSON/JSONEncoderImpl.swift | 630 +++++++++++++++++++++ Sources/ReerJSON/ReerJSONEncoder.swift | 357 ++++++++++++ Sources/ReerJSON/Utilities.swift | 4 + Tests/ReerJSONTests/JSONEncoderTests.swift | 102 ++-- 4 files changed, 1042 insertions(+), 51 deletions(-) create mode 100644 Sources/ReerJSON/JSONEncoderImpl.swift create mode 100644 Sources/ReerJSON/ReerJSONEncoder.swift diff --git a/Sources/ReerJSON/JSONEncoderImpl.swift b/Sources/ReerJSON/JSONEncoderImpl.swift new file mode 100644 index 0000000..e98db3b --- /dev/null +++ b/Sources/ReerJSON/JSONEncoderImpl.swift @@ -0,0 +1,630 @@ +// +// Copyright © 2024 swiftlang. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Copyright © 2025 reers. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import yyjson + +#if !os(Linux) +import JJLISO8601DateFormatter +#endif + +class JSONEncoderImpl: Encoder { + let doc: UnsafeMutablePointer + let options: ReerJSONEncoder.Options + var codingPath: [CodingKey] + + var userInfo: [CodingUserInfoKey: Any] { + return options.userInfo + } + + var singleValue: UnsafeMutablePointer? + var array: UnsafeMutablePointer? + var object: UnsafeMutablePointer? + + init(doc: UnsafeMutablePointer, codingPath: [CodingKey], options: ReerJSONEncoder.Options) { + self.doc = doc + self.codingPath = codingPath + self.options = options + } + + func takeValue() -> UnsafeMutablePointer? { + if let object { + return object + } + if let array { + return array + } + return singleValue + } + + // MARK: - Encoder Methods + + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { + if let object { + let container = YYJSONKeyedEncodingContainer(impl: self, codingPath: codingPath, object: object) + return KeyedEncodingContainer(container) + } + + if let sv = singleValue, yyjson_mut_is_obj(sv) { + self.object = sv + self.singleValue = nil + let container = YYJSONKeyedEncodingContainer(impl: self, codingPath: codingPath, object: sv) + return KeyedEncodingContainer(container) + } + + guard singleValue == nil, array == nil else { + preconditionFailure("Attempt to push new keyed encoding container when already previously encoded at this path.") + } + + let obj = yyjson_mut_obj(doc)! + self.object = obj + let container = YYJSONKeyedEncodingContainer(impl: self, codingPath: codingPath, object: obj) + return KeyedEncodingContainer(container) + } + + func unkeyedContainer() -> UnkeyedEncodingContainer { + if let array { + return YYJSONUnkeyedEncodingContainer(impl: self, codingPath: codingPath, array: array) + } + + if let sv = singleValue, yyjson_mut_is_arr(sv) { + self.array = sv + self.singleValue = nil + return YYJSONUnkeyedEncodingContainer(impl: self, codingPath: codingPath, array: sv) + } + + guard singleValue == nil, object == nil else { + preconditionFailure("Attempt to push new unkeyed encoding container when already previously encoded at this path.") + } + + let arr = yyjson_mut_arr(doc)! + self.array = arr + return YYJSONUnkeyedEncodingContainer(impl: self, codingPath: codingPath, array: arr) + } + + func singleValueContainer() -> SingleValueEncodingContainer { + return self + } + + // MARK: - Value Creation Helpers + + @inline(__always) + func wrapInt(_ value: some FixedWidthInteger & SignedInteger) -> UnsafeMutablePointer { + return yyjson_mut_sint(doc, Int64(value)) + } + + @inline(__always) + func wrapUInt(_ value: some FixedWidthInteger & UnsignedInteger) -> UnsafeMutablePointer { + return yyjson_mut_uint(doc, UInt64(value)) + } + + #if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + func wrapInt128(_ value: Int128) -> UnsafeMutablePointer { + let str = String(value) + return yyjson_mut_rawcpy(doc, str) + } + + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + func wrapUInt128(_ value: UInt128) -> UnsafeMutablePointer { + let str = String(value) + return yyjson_mut_rawcpy(doc, str) + } + #endif + + @inline(__always) + func wrapDouble(_ value: Double) -> UnsafeMutablePointer { + let str = formatDouble(value) + return yyjson_mut_rawcpy(doc, str) + } + + private func formatDouble(_ value: Double) -> String { + var string = value.description + if string.hasSuffix(".0") { + string.removeLast(2) + } + return string + } + + func wrapFloat(_ float: T, for additionalKey: CodingKey?) throws -> UnsafeMutablePointer { + guard !float.isNaN, !float.isInfinite else { + if case .convertToString(let posInfString, let negInfString, let nanString) = options.nonConformingFloatEncodingStrategy { + switch float { + case T.infinity: + return wrapString(posInfString) + case -T.infinity: + return wrapString(negInfString) + default: + return wrapString(nanString) + } + } + + var path = codingPath + if let additionalKey { path.append(additionalKey) } + throw EncodingError.invalidValue(float, .init( + codingPath: path, + debugDescription: "Unable to encode \(T.self).\(float) directly in JSON." + )) + } + + var string = float.description + if string.hasSuffix(".0") { + string.removeLast(2) + } + return yyjson_mut_rawcpy(doc, string) + } + + @inline(__always) + func wrapString(_ string: String) -> UnsafeMutablePointer { + return string.withCString { cStr in + let len = string.utf8.count + return yyjson_mut_strncpy(doc, cStr, len) + } + } + + func wrapEncodable(_ value: Encodable, for additionalKey: CodingKey?) throws -> UnsafeMutablePointer? { + if let date = value as? Date { + return try wrapDateValue(date, for: additionalKey) + } else if let data = value as? Data { + return try wrapDataValue(data, for: additionalKey) + } else if let url = value as? URL { + return wrapString(url.absoluteString) + } else if let decimal = value as? Decimal { + return yyjson_mut_rawcpy(doc, decimal.description) + } else if value is _JSONStringDictionaryEncodableMarker, let dict = value as? [String: Encodable] { + return try wrapStringKeyedDictValue(dict, for: additionalKey) + } + #if compiler(>=6.0) + if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { + if let i128 = value as? Int128 { + return wrapInt128(i128) + } + if let u128 = value as? UInt128 { + return wrapUInt128(u128) + } + } + #endif + + let newPath = codingPath + (additionalKey.map { [$0] } ?? []) + let subEncoder = JSONEncoderImpl(doc: doc, codingPath: newPath, options: options) + try value.encode(to: subEncoder) + return subEncoder.takeValue() + } + + func wrapDateValue(_ date: Date, for additionalKey: CodingKey?) throws -> UnsafeMutablePointer? { + switch options.dateEncodingStrategy { + case .deferredToDate: + let newPath = codingPath + (additionalKey.map { [$0] } ?? []) + let subEncoder = JSONEncoderImpl(doc: doc, codingPath: newPath, options: options) + try date.encode(to: subEncoder) + return subEncoder.takeValue() + + case .secondsSince1970: + return try wrapFloat(date.timeIntervalSince1970, for: additionalKey) + + case .millisecondsSince1970: + return try wrapFloat(1000.0 * date.timeIntervalSince1970, for: additionalKey) + + case .iso8601: + let string = _iso8601Formatter.string(from: date) + return wrapString(string) + + case .formatted(let formatter): + let string = formatter.string(from: date) + return wrapString(string) + + case .custom(let closure): + let newPath = codingPath + (additionalKey.map { [$0] } ?? []) + let subEncoder = JSONEncoderImpl(doc: doc, codingPath: newPath, options: options) + try closure(date, subEncoder) + return subEncoder.takeValue() ?? yyjson_mut_obj(doc) + + @unknown default: + fatalError() + } + } + + func wrapDataValue(_ data: Data, for additionalKey: CodingKey?) throws -> UnsafeMutablePointer? { + switch options.dataEncodingStrategy { + case .deferredToData: + let newPath = codingPath + (additionalKey.map { [$0] } ?? []) + let subEncoder = JSONEncoderImpl(doc: doc, codingPath: newPath, options: options) + try data.encode(to: subEncoder) + return subEncoder.takeValue() + + case .base64: + let base64 = data.base64EncodedString() + return wrapString(base64) + + case .custom(let closure): + let newPath = codingPath + (additionalKey.map { [$0] } ?? []) + let subEncoder = JSONEncoderImpl(doc: doc, codingPath: newPath, options: options) + try closure(data, subEncoder) + return subEncoder.takeValue() ?? yyjson_mut_obj(doc) + + @unknown default: + fatalError() + } + } + + func wrapStringKeyedDictValue(_ dict: [String: Encodable], for additionalKey: CodingKey?) throws -> UnsafeMutablePointer? { + let obj = yyjson_mut_obj(doc)! + let savedCodingPath = codingPath + if let additionalKey { + codingPath.append(additionalKey) + } + for (key, value) in dict { + let keyVal = wrapString(key) + let dictKey = _CodingKey(stringValue: key)! + codingPath.append(dictKey) + let subEncoder = JSONEncoderImpl(doc: doc, codingPath: codingPath, options: options) + let val = try subEncoder.wrapEncodable(value, for: nil) ?? yyjson_mut_obj(doc)! + yyjson_mut_obj_add(obj, keyVal, val) + codingPath.removeLast() + } + codingPath = savedCodingPath + return obj + } + + // MARK: - Key encoding strategy + + func convertedKey(_ key: CodingKey) -> String { + switch options.keyEncodingStrategy { + case .useDefaultKeys: + return key.stringValue + case .convertToSnakeCase: + return Self._convertToSnakeCase(key.stringValue) + case .custom(let converter): + return converter(codingPath + [key]).stringValue + @unknown default: + return key.stringValue + } + } + + static func _convertToSnakeCase(_ stringKey: String) -> String { + guard !stringKey.isEmpty else { return stringKey } + + var words: [Range] = [] + var wordStart = stringKey.startIndex + var searchRange = stringKey.index(after: wordStart)..=6.0) + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + func encode(_ value: Int128) throws { + singleValue = wrapInt128(value) + } + + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + func encode(_ value: UInt128) throws { + singleValue = wrapUInt128(value) + } + #endif + + func encode(_ value: T) throws { + #if compiler(>=6.0) + if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { + if let i128 = value as? Int128 { + singleValue = wrapInt128(i128) + return + } + if let u128 = value as? UInt128 { + singleValue = wrapUInt128(u128) + return + } + } + #endif + singleValue = try wrapEncodable(value, for: nil) ?? yyjson_mut_obj(doc) + } +} + +// MARK: - YYJSONKeyedEncodingContainer + +private struct YYJSONKeyedEncodingContainer: KeyedEncodingContainerProtocol { + typealias Key = K + + let impl: JSONEncoderImpl + var codingPath: [CodingKey] + let object: UnsafeMutablePointer + + var doc: UnsafeMutablePointer { impl.doc } + + init(impl: JSONEncoderImpl, codingPath: [CodingKey], object: UnsafeMutablePointer) { + self.impl = impl + self.codingPath = codingPath + self.object = object + } + + private func addToObject(key: String, value: UnsafeMutablePointer) { + let keyVal = impl.wrapString(key) + yyjson_mut_obj_put(object, keyVal, value) + } + + mutating func encodeNil(forKey key: Key) throws { + addToObject(key: impl.convertedKey(key), value: yyjson_mut_null(doc)) + } + + mutating func encode(_ value: Bool, forKey key: Key) throws { + addToObject(key: impl.convertedKey(key), value: yyjson_mut_bool(doc, value)) + } + + mutating func encode(_ value: String, forKey key: Key) throws { + addToObject(key: impl.convertedKey(key), value: impl.wrapString(value)) + } + + mutating func encode(_ value: Double, forKey key: Key) throws { + addToObject(key: impl.convertedKey(key), value: try impl.wrapFloat(value, for: key)) + } + + mutating func encode(_ value: Float, forKey key: Key) throws { + addToObject(key: impl.convertedKey(key), value: try impl.wrapFloat(value, for: key)) + } + + mutating func encode(_ value: Int, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapInt(value)) } + mutating func encode(_ value: Int8, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapInt(value)) } + mutating func encode(_ value: Int16, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapInt(value)) } + mutating func encode(_ value: Int32, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapInt(value)) } + mutating func encode(_ value: Int64, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapInt(value)) } + mutating func encode(_ value: UInt, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapUInt(value)) } + mutating func encode(_ value: UInt8, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapUInt(value)) } + mutating func encode(_ value: UInt16, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapUInt(value)) } + mutating func encode(_ value: UInt32, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapUInt(value)) } + mutating func encode(_ value: UInt64, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapUInt(value)) } + + #if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + mutating func encode(_ value: Int128, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapInt128(value)) } + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + mutating func encode(_ value: UInt128, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapUInt128(value)) } + #endif + + mutating func encode(_ value: T, forKey key: Key) throws { + let convertedKeyStr = impl.convertedKey(key) + let val = try impl.wrapEncodable(value, for: key) ?? yyjson_mut_obj(doc)! + addToObject(key: convertedKeyStr, value: val) + } + + mutating func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer { + let convertedKeyStr = impl.convertedKey(key) + let existingVal = convertedKeyStr.withCString { cStr in + yyjson_mut_obj_getn(object, cStr, convertedKeyStr.utf8.count) + } + if let existingVal, yyjson_mut_is_obj(existingVal) { + let container = YYJSONKeyedEncodingContainer(impl: impl, codingPath: codingPath + [key], object: existingVal) + return KeyedEncodingContainer(container) + } + let nestedObj = yyjson_mut_obj(doc)! + addToObject(key: convertedKeyStr, value: nestedObj) + let container = YYJSONKeyedEncodingContainer(impl: impl, codingPath: codingPath + [key], object: nestedObj) + return KeyedEncodingContainer(container) + } + + mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + let convertedKeyStr = impl.convertedKey(key) + let nestedArr = yyjson_mut_arr(doc)! + addToObject(key: convertedKeyStr, value: nestedArr) + return YYJSONUnkeyedEncodingContainer(impl: impl, codingPath: codingPath + [key], array: nestedArr) + } + + mutating func superEncoder() -> Encoder { + let convertedKeyStr = impl.convertedKey(_CodingKey.super) + return YYJSONReferencingEncoder(impl: impl, key: convertedKeyStr, codingPath: codingPath + [_CodingKey.super], object: object) as Encoder + } + + mutating func superEncoder(forKey key: Key) -> Encoder { + let convertedKeyStr = impl.convertedKey(key) + return YYJSONReferencingEncoder(impl: impl, key: convertedKeyStr, codingPath: codingPath + [key], object: object) as Encoder + } +} + +// MARK: - YYJSONUnkeyedEncodingContainer + +private struct YYJSONUnkeyedEncodingContainer: UnkeyedEncodingContainer { + let impl: JSONEncoderImpl + var codingPath: [CodingKey] + let array: UnsafeMutablePointer + + var doc: UnsafeMutablePointer { impl.doc } + + var count: Int { + Int(yyjson_mut_arr_size(array)) + } + + init(impl: JSONEncoderImpl, codingPath: [CodingKey], array: UnsafeMutablePointer) { + self.impl = impl + self.codingPath = codingPath + self.array = array + } + + mutating func encodeNil() throws { + yyjson_mut_arr_add_null(doc, array) + } + + mutating func encode(_ value: Bool) throws { + yyjson_mut_arr_add_bool(doc, array, value) + } + + mutating func encode(_ value: String) throws { + let val = impl.wrapString(value) + yyjson_mut_arr_append(array, val) + } + + mutating func encode(_ value: Double) throws { + let val = try impl.wrapFloat(value, for: _CodingKey(index: count)) + yyjson_mut_arr_append(array, val) + } + + mutating func encode(_ value: Float) throws { + let val = try impl.wrapFloat(value, for: _CodingKey(index: count)) + yyjson_mut_arr_append(array, val) + } + + mutating func encode(_ value: Int) throws { yyjson_mut_arr_add_sint(doc, array, Int64(value)) } + mutating func encode(_ value: Int8) throws { yyjson_mut_arr_add_sint(doc, array, Int64(value)) } + mutating func encode(_ value: Int16) throws { yyjson_mut_arr_add_sint(doc, array, Int64(value)) } + mutating func encode(_ value: Int32) throws { yyjson_mut_arr_add_sint(doc, array, Int64(value)) } + mutating func encode(_ value: Int64) throws { yyjson_mut_arr_add_sint(doc, array, Int64(value)) } + mutating func encode(_ value: UInt) throws { yyjson_mut_arr_add_uint(doc, array, UInt64(value)) } + mutating func encode(_ value: UInt8) throws { yyjson_mut_arr_add_uint(doc, array, UInt64(value)) } + mutating func encode(_ value: UInt16) throws { yyjson_mut_arr_add_uint(doc, array, UInt64(value)) } + mutating func encode(_ value: UInt32) throws { yyjson_mut_arr_add_uint(doc, array, UInt64(value)) } + mutating func encode(_ value: UInt64) throws { yyjson_mut_arr_add_uint(doc, array, UInt64(value)) } + + #if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + mutating func encode(_ value: Int128) throws { + let val = impl.wrapInt128(value) + yyjson_mut_arr_append(array, val) + } + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + mutating func encode(_ value: UInt128) throws { + let val = impl.wrapUInt128(value) + yyjson_mut_arr_append(array, val) + } + #endif + + mutating func encode(_ value: T) throws { + let val = try impl.wrapEncodable(value, for: _CodingKey(index: count)) ?? yyjson_mut_obj(doc)! + yyjson_mut_arr_append(array, val) + } + + mutating func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer { + let nestedObj = yyjson_mut_obj(doc)! + yyjson_mut_arr_append(array, nestedObj) + let container = YYJSONKeyedEncodingContainer(impl: impl, codingPath: codingPath + [_CodingKey(index: count - 1)], object: nestedObj) + return KeyedEncodingContainer(container) + } + + mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + let nestedArr = yyjson_mut_arr(doc)! + yyjson_mut_arr_append(array, nestedArr) + return YYJSONUnkeyedEncodingContainer(impl: impl, codingPath: codingPath + [_CodingKey(index: count - 1)], array: nestedArr) + } + + mutating func superEncoder() -> Encoder { + let idx = count + return YYJSONReferencingArrayEncoder(impl: impl, codingPath: codingPath + [_CodingKey(index: idx)], array: array, index: idx) + } +} + +// MARK: - YYJSONReferencingEncoder (for keyed containers) + +private class YYJSONReferencingEncoder: JSONEncoderImpl { + let key: String + let referencedObject: UnsafeMutablePointer + + init(impl: JSONEncoderImpl, key: String, codingPath: [CodingKey], object: UnsafeMutablePointer) { + self.key = key + self.referencedObject = object + super.init(doc: impl.doc, codingPath: codingPath, options: impl.options) + } + + deinit { + let value = takeValue() ?? yyjson_mut_obj(doc)! + let keyVal = wrapString(key) + yyjson_mut_obj_put(referencedObject, keyVal, value) + } +} + +// MARK: - YYJSONReferencingArrayEncoder (for unkeyed containers) + +private class YYJSONReferencingArrayEncoder: JSONEncoderImpl { + let referencedArray: UnsafeMutablePointer + let insertIndex: Int + + init(impl: JSONEncoderImpl, codingPath: [CodingKey], array: UnsafeMutablePointer, index: Int) { + self.referencedArray = array + self.insertIndex = index + super.init(doc: impl.doc, codingPath: codingPath, options: impl.options) + } + + deinit { + let value = takeValue() ?? yyjson_mut_obj(doc)! + yyjson_mut_arr_insert(referencedArray, value, insertIndex) + } +} diff --git a/Sources/ReerJSON/ReerJSONEncoder.swift b/Sources/ReerJSON/ReerJSONEncoder.swift new file mode 100644 index 0000000..3cf1e4c --- /dev/null +++ b/Sources/ReerJSON/ReerJSONEncoder.swift @@ -0,0 +1,357 @@ +// +// Copyright © 2024 swiftlang. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Copyright © 2025 reers. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import yyjson + +open class ReerJSONEncoder { + + open var outputFormatting: JSONEncoder.OutputFormatting { + get { + optionsLock.lock() + defer { optionsLock.unlock() } + return options.outputFormatting + } + _modify { + optionsLock.lock() + var value = options.outputFormatting + defer { + options.outputFormatting = value + optionsLock.unlock() + } + yield &value + } + set { + optionsLock.lock() + defer { optionsLock.unlock() } + options.outputFormatting = newValue + } + } + + open var dateEncodingStrategy: JSONEncoder.DateEncodingStrategy { + get { + optionsLock.lock() + defer { optionsLock.unlock() } + return options.dateEncodingStrategy + } + _modify { + optionsLock.lock() + var value = options.dateEncodingStrategy + defer { + options.dateEncodingStrategy = value + optionsLock.unlock() + } + yield &value + } + set { + optionsLock.lock() + defer { optionsLock.unlock() } + options.dateEncodingStrategy = newValue + } + } + + open var dataEncodingStrategy: JSONEncoder.DataEncodingStrategy { + get { + optionsLock.lock() + defer { optionsLock.unlock() } + return options.dataEncodingStrategy + } + _modify { + optionsLock.lock() + var value = options.dataEncodingStrategy + defer { + options.dataEncodingStrategy = value + optionsLock.unlock() + } + yield &value + } + set { + optionsLock.lock() + defer { optionsLock.unlock() } + options.dataEncodingStrategy = newValue + } + } + + open var nonConformingFloatEncodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy { + get { + optionsLock.lock() + defer { optionsLock.unlock() } + return options.nonConformingFloatEncodingStrategy + } + _modify { + optionsLock.lock() + var value = options.nonConformingFloatEncodingStrategy + defer { + options.nonConformingFloatEncodingStrategy = value + optionsLock.unlock() + } + yield &value + } + set { + optionsLock.lock() + defer { optionsLock.unlock() } + options.nonConformingFloatEncodingStrategy = newValue + } + } + + open var keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy { + get { + optionsLock.lock() + defer { optionsLock.unlock() } + return options.keyEncodingStrategy + } + _modify { + optionsLock.lock() + var value = options.keyEncodingStrategy + defer { + options.keyEncodingStrategy = value + optionsLock.unlock() + } + yield &value + } + set { + optionsLock.lock() + defer { optionsLock.unlock() } + options.keyEncodingStrategy = newValue + } + } + + @preconcurrency + open var userInfo: [CodingUserInfoKey: any Sendable] { + get { + optionsLock.lock() + defer { optionsLock.unlock() } + return options.userInfo + } + _modify { + optionsLock.lock() + var value = options.userInfo + defer { + options.userInfo = value + optionsLock.unlock() + } + yield &value + } + set { + optionsLock.lock() + defer { optionsLock.unlock() } + options.userInfo = newValue + } + } + + struct Options { + var outputFormatting: JSONEncoder.OutputFormatting = [] + var dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .deferredToDate + var dataEncodingStrategy: JSONEncoder.DataEncodingStrategy = .base64 + var nonConformingFloatEncodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy = .throw + var keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy = .useDefaultKeys + var userInfo: [CodingUserInfoKey: any Sendable] = [:] + } + + fileprivate var options = Options() + fileprivate let optionsLock = LockedState() + + public init() {} + + open func encode(_ value: T) throws -> Data { + let doc = yyjson_mut_doc_new(nil)! + defer { yyjson_mut_doc_free(doc) } + + let encoder = JSONEncoderImpl(doc: doc, codingPath: [], options: options) + + if let date = value as? Date { + encoder.singleValue = try encoder.wrapDateValue(date, for: nil) + } else if let data = value as? Data { + encoder.singleValue = try encoder.wrapDataValue(data, for: nil) + } else if let url = value as? URL { + encoder.singleValue = encoder.wrapString(url.absoluteString) + } else if let decimal = value as? Decimal { + encoder.singleValue = yyjson_mut_rawcpy(doc, decimal.description) + } else if value is _JSONStringDictionaryEncodableMarker, let dict = value as? [String: Encodable] { + encoder.singleValue = try encoder.wrapStringKeyedDictValue(dict, for: nil) + } else { + try value.encode(to: encoder) + } + + guard var root = encoder.takeValue() else { + throw EncodingError.invalidValue(value, EncodingError.Context( + codingPath: [], + debugDescription: "Top-level \(T.self) did not encode any values." + )) + } + + let sortKeys = outputFormatting.contains(.sortedKeys) + + if sortKeys { + root = sortMutVal(root, doc: doc) + } + + yyjson_mut_doc_set_root(doc, root) + + var writeFlag: yyjson_write_flag = YYJSON_WRITE_NOFLAG + if outputFormatting.contains(.prettyPrinted) { + writeFlag |= YYJSON_WRITE_PRETTY_TWO_SPACES + } + if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { + if !outputFormatting.contains(.withoutEscapingSlashes) { + writeFlag |= YYJSON_WRITE_ESCAPE_SLASHES + } + } else { + writeFlag |= YYJSON_WRITE_ESCAPE_SLASHES + } + + var len: Int = 0 + guard let cstr = yyjson_mut_write(doc, writeFlag, &len) else { + throw EncodingError.invalidValue(value, EncodingError.Context( + codingPath: [], + debugDescription: "Unable to encode the given top-level value to JSON." + )) + } + defer { free(cstr) } + lowercaseUnicodeEscapes(cstr, len) + + if outputFormatting.contains(.prettyPrinted) { + var data = Data(bytes: cstr, count: len) + addSpaceBeforeColonInPrettyJSON(&data) + return data + } + return Data(bytes: cstr, count: len) + } + + private func addSpaceBeforeColonInPrettyJSON(_ data: inout Data) { + // yyjson outputs "key": value but Foundation outputs "key" : value + var result = Data() + result.reserveCapacity(data.count + data.count / 10) + + var i = 0 + let bytes = Array(data) + let count = bytes.count + var inString = false + var escaped = false + + while i < count { + let byte = bytes[i] + + if escaped { + result.append(byte) + escaped = false + i += 1 + continue + } + + if byte == 0x5C /* \ */ && inString { + result.append(byte) + escaped = true + i += 1 + continue + } + + if byte == 0x22 /* " */ { + inString = !inString + result.append(byte) + i += 1 + continue + } + + if !inString && byte == 0x3A /* : */ && i > 0 && bytes[i-1] == 0x22 /* " */ { + result.append(0x20) // space + result.append(byte) // : + i += 1 + continue + } + + result.append(byte) + i += 1 + } + + data = result + } + + private func lowercaseUnicodeEscapes(_ buf: UnsafeMutablePointer, _ len: Int) { + let ptr = UnsafeMutableRawPointer(buf).assumingMemoryBound(to: UInt8.self) + var i = 0 + while i < len - 5 { + if ptr[i] == 0x5C /* \ */ && ptr[i+1] == 0x75 /* u */ { + for j in (i+2)...(i+5) { + let c = ptr[j] + if c >= 0x41 && c <= 0x46 { // A-F + ptr[j] = c + 32 // a-f + } + } + i += 6 + } else { + i += 1 + } + } + } + + private func sortMutVal(_ val: UnsafeMutablePointer, doc: UnsafeMutablePointer) -> UnsafeMutablePointer { + if yyjson_mut_is_obj(val) { + let newObj = yyjson_mut_obj(doc)! + + var pairs: [(key: String, keyVal: UnsafeMutablePointer, valVal: UnsafeMutablePointer)] = [] + + var iter = yyjson_mut_obj_iter() + yyjson_mut_obj_iter_init(val, &iter) + while let keyPtr = yyjson_mut_obj_iter_next(&iter) { + guard let valPtr = yyjson_mut_obj_iter_get_val(keyPtr) else { continue } + let keyStr: String + if let cStr = yyjson_mut_get_str(keyPtr) { + keyStr = String(cString: cStr) + } else { + keyStr = "" + } + pairs.append((key: keyStr, keyVal: keyPtr, valVal: valPtr)) + } + + pairs.sort { a, b in + a.key.utf8.lexicographicallyPrecedes(b.key.utf8) + } + + for pair in pairs { + let sortedVal = sortMutVal(pair.valVal, doc: doc) + let newKey = yyjson_mut_strcpy(doc, pair.key)! + yyjson_mut_obj_add(newObj, newKey, sortedVal) + } + + return newObj + } else if yyjson_mut_is_arr(val) { + let newArr = yyjson_mut_arr(doc)! + + var iter = yyjson_mut_arr_iter() + yyjson_mut_arr_iter_init(val, &iter) + while let elemPtr = yyjson_mut_arr_iter_next(&iter) { + let sortedElem = sortMutVal(elemPtr, doc: doc) + yyjson_mut_arr_append(newArr, sortedElem) + } + + return newArr + } + + return val + } +} diff --git a/Sources/ReerJSON/Utilities.swift b/Sources/ReerJSON/Utilities.swift index 97ebc66..9ee911e 100644 --- a/Sources/ReerJSON/Utilities.swift +++ b/Sources/ReerJSON/Utilities.swift @@ -122,6 +122,10 @@ protocol StringDecodableDictionary { static var elementType: Decodable.Type { get } } +protocol _JSONStringDictionaryEncodableMarker {} + +extension Dictionary: _JSONStringDictionaryEncodableMarker where Key == String, Value: Encodable {} + extension Dictionary: StringDecodableDictionary where Key == String, Value: Decodable { static var elementType: Decodable.Type { return Value.self } } diff --git a/Tests/ReerJSONTests/JSONEncoderTests.swift b/Tests/ReerJSONTests/JSONEncoderTests.swift index 8e480d1..69377a4 100644 --- a/Tests/ReerJSONTests/JSONEncoderTests.swift +++ b/Tests/ReerJSONTests/JSONEncoderTests.swift @@ -96,18 +96,18 @@ private struct JSONEncoderTests { @Test func encodingTopLevelArrayOfInt() throws { let a = [1,2,3] - let result1 = String(data: try JSONEncoder().encode(a), encoding: .utf8) + let result1 = String(data: try ReerJSONEncoder().encode(a), encoding: .utf8) #expect(result1 == "[1,2,3]") let b : [Int] = [] - let result2 = String(data: try JSONEncoder().encode(b), encoding: .utf8) + let result2 = String(data: try ReerJSONEncoder().encode(b), encoding: .utf8) #expect(result2 == "[]") } // @Test func encodingTopLevelWithConfiguration() throws { // // CodableTypeWithConfiguration is a struct that conforms to CodableWithConfiguration // let value = CodableTypeWithConfiguration.testValue -// let encoder = JSONEncoder() +// let encoder = ReerJSONEncoder() // let decoder = JSONDecoder() // // var decoded = try decoder.decode( @@ -155,7 +155,7 @@ private struct JSONEncoderTests { await #expect(processExitsWith: .failure) { let model = Model.testValue // This following test would fail as it attempts to re-encode into already encoded container is invalid. This will always fail - _ = try JSONEncoder().encode(model) + _ = try ReerJSONEncoder().encode(model) } } #endif @@ -438,7 +438,7 @@ private struct JSONEncoderTests { let expected = "{\"QQQhello\":\"test\"}" let encoded = EncodeMe(keyName: "hello") - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() let customKeyConversion = { @Sendable (_ path : [CodingKey]) -> CodingKey in let key = _TestKey(stringValue: "QQQ" + path.last!.stringValue)! return key @@ -472,7 +472,7 @@ private struct JSONEncoderTests { let expected = "{\"QQQouterValue\":{\"QQQnestedValue\":{\"QQQhelloWorld\":\"test\"}}}" let encoded = EncodeNestedNested(outerValue: EncodeNested(nestedValue: EncodeMe(keyName: "helloWorld"))) - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() // We only will mutate this from one thread as we call the encoder synchronously nonisolated(unsafe) var callCount = 0 @@ -619,7 +619,7 @@ private struct JSONEncoderTests { // Encoding let encoded = DecodeMe5() - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.keyEncodingStrategy = .custom(customKeyConversion) let decodingResultData = try encoder.encode(encoded) let decodingResultString = String(bytes: decodingResultData, encoding: .utf8) @@ -630,14 +630,14 @@ private struct JSONEncoderTests { // MARK: - Encoder Features @Test func nestedContainerCodingPaths() { - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() #expect(throws: Never.self) { try encoder.encode(NestedContainersTestType()) } } @Test func superEncoderCodingPaths() { - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() #expect(throws: Never.self) { try encoder.encode(NestedContainersTestType(testSuperEncoder: true)) } @@ -683,7 +683,7 @@ private struct JSONEncoderTests { } @Test func decodingConcreteTypeParameter() throws { - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() let json = try encoder.encode(Employee.testValue) let decoder = JSONDecoder() @@ -720,7 +720,7 @@ private struct JSONEncoderTests { // // The issue at hand reproduces when you have a referencing encoder (superEncoder() creates one) that has a container on the stack (unkeyedContainer() adds one) that encodes a value going through box_() (Array does that) that encodes something which throws (Float.infinity does that). // When reproducing, this will cause a test failure via fatalError(). - _ = try? JSONEncoder().encode(ReferencingEncoderWrapper([Float.infinity])) + _ = try? ReerJSONEncoder().encode(ReferencingEncoderWrapper([Float.infinity])) } @Test func encoderStateThrowOnEncodeCustomDate() { @@ -737,7 +737,7 @@ private struct JSONEncoderTests { } // The closure needs to push a container before throwing an error to trigger. - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.dateEncodingStrategy = .custom({ _, encoder in let _ = encoder.unkeyedContainer() enum CustomError : Error { case foo } @@ -761,7 +761,7 @@ private struct JSONEncoderTests { } // The closure needs to push a container before throwing an error to trigger. - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.dataEncodingStrategy = .custom({ _, encoder in let _ = encoder.unkeyedContainer() enum CustomError : Error { case foo } @@ -801,7 +801,7 @@ private struct JSONEncoderTests { } let value = Level1.init(level2: .init(name: "level2")) - let data = try JSONEncoder().encode(value) + let data = try ReerJSONEncoder().encode(value) let decodedValue = try JSONDecoder().decode(Level1.self, from: data) #expect(value == decodedValue) @@ -985,7 +985,7 @@ private struct JSONEncoderTests { } let before = DelayedDecodable_ContainerVersion(42) - let data = try JSONEncoder().encode(before) + let data = try ReerJSONEncoder().encode(before) let decoded = try JSONDecoder().decode(DelayedDecodable_ContainerVersion.self, from: data) #expect(throws: Never.self) { @@ -1033,7 +1033,7 @@ private struct JSONEncoderTests { private func _testEncodeFailure(of value: T, sourceLocation: SourceLocation = #_sourceLocation) { #expect(throws: (any Error).self, "Encode of top-level \(T.self) was expected to fail.", sourceLocation: sourceLocation) { - try JSONEncoder().encode(value) + try ReerJSONEncoder().encode(value) } } @@ -1057,7 +1057,7 @@ private struct JSONEncoderTests { sourceLocation: SourceLocation = #_sourceLocation) where T : Codable, T : Equatable { var payload: Data do { - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.outputFormatting = outputFormatting encoder.dateEncodingStrategy = dateEncodingStrategy encoder.dataEncodingStrategy = dataEncodingStrategy @@ -1090,7 +1090,7 @@ private struct JSONEncoderTests { private func _testRoundTripTypeCoercionFailure(of value: T, as type: U.Type, sourceLocation: SourceLocation = #_sourceLocation) where T : Codable, U : Codable { #expect(throws: (any Error).self, "Coercion from \(T.self) to \(U.self) was expected to fail.", sourceLocation: sourceLocation) { - let data = try JSONEncoder().encode(value) + let data = try ReerJSONEncoder().encode(value) let _ = try JSONDecoder().decode(U.self, from: data) } } @@ -1172,7 +1172,7 @@ private struct JSONEncoderTests { @Test func nullByte() throws { let string = "abc\u{0000}def" - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() let decoder = JSONDecoder() let data = try encoder.encode([string]) @@ -1511,7 +1511,7 @@ private struct JSONEncoderTests { } @Test func decodeLargeDoubleAsInteger() throws { - let data = try JSONEncoder().encode(Double.greatestFiniteMagnitude) + let data = try ReerJSONEncoder().encode(Double.greatestFiniteMagnitude) #expect(throws: (any Error).self) { try JSONDecoder().decode(UInt64.self, from: data) } @@ -1533,7 +1533,7 @@ private struct JSONEncoderTests { let orig = ["decimalValue" : 1.1] setlocale(LC_ALL, "fr_FR") - let data = try JSONEncoder().encode(orig) + let data = try ReerJSONEncoder().encode(orig) #if os(Windows) setlocale(LC_ALL, "en_US") @@ -1668,7 +1668,7 @@ private struct JSONEncoderTests { @Test func infiniteDate() { let date = Date(timeIntervalSince1970: .infinity) - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.dateEncodingStrategy = .deferredToDate #expect(throws: (any Error).self) { @@ -1692,7 +1692,7 @@ private struct JSONEncoderTests { // Intentionally nothing. } } - let enc = JSONEncoder() + let enc = ReerJSONEncoder() #expect(throws: (any Error).self) { try enc.encode(EncodesNothing()) @@ -1741,7 +1741,7 @@ private struct JSONEncoderTests { // NOTE!!! At present, the order in which the values in the unkeyed container's superEncoders above get inserted into the resulting array depends on the order in which the superEncoders are deinit'd!! This can result in some very unexpected results, and this pattern is not recommended. This test exists just to verify compatibility. } } - let data = try JSONEncoder().encode(SuperEncoding()) + let data = try ReerJSONEncoder().encode(SuperEncoding()) let string = String(data: data, encoding: .utf8)! #expect(string.contains("\"firstSuper\":\"First\"")) @@ -1783,22 +1783,22 @@ private struct JSONEncoderTests { } } } - var data = try JSONEncoder().encode(RedundantEncoding(replacedType: .value, useSuperEncoder: false)) + var data = try ReerJSONEncoder().encode(RedundantEncoding(replacedType: .value, useSuperEncoder: false)) #expect(String(data: data, encoding: .utf8) == ("{\"key\":42}")) - data = try JSONEncoder().encode(RedundantEncoding(replacedType: .value, useSuperEncoder: true)) + data = try ReerJSONEncoder().encode(RedundantEncoding(replacedType: .value, useSuperEncoder: true)) #expect(String(data: data, encoding: .utf8) == ("{\"key\":42}")) - data = try JSONEncoder().encode(RedundantEncoding(replacedType: .keyedContainer, useSuperEncoder: false)) + data = try ReerJSONEncoder().encode(RedundantEncoding(replacedType: .keyedContainer, useSuperEncoder: false)) #expect(String(data: data, encoding: .utf8) == ("{\"key\":42}")) - data = try JSONEncoder().encode(RedundantEncoding(replacedType: .keyedContainer, useSuperEncoder: true)) + data = try ReerJSONEncoder().encode(RedundantEncoding(replacedType: .keyedContainer, useSuperEncoder: true)) #expect(String(data: data, encoding: .utf8) == ("{\"key\":42}")) - data = try JSONEncoder().encode(RedundantEncoding(replacedType: .unkeyedContainer, useSuperEncoder: false)) + data = try ReerJSONEncoder().encode(RedundantEncoding(replacedType: .unkeyedContainer, useSuperEncoder: false)) #expect(String(data: data, encoding: .utf8) == ("{\"key\":42}")) - data = try JSONEncoder().encode(RedundantEncoding(replacedType: .unkeyedContainer, useSuperEncoder: true)) + data = try ReerJSONEncoder().encode(RedundantEncoding(replacedType: .unkeyedContainer, useSuperEncoder: true)) #expect(String(data: data, encoding: .utf8) == ("{\"key\":42}")) } @@ -1830,7 +1830,7 @@ private struct JSONEncoderTests { } let toEncode = Something(dict: [:]) - let data = try JSONEncoder().encode(toEncode) + let data = try ReerJSONEncoder().encode(toEncode) let result = try JSONDecoder().decode(Something.self, from: data) #expect(result.dict.count == 0) } @@ -1871,42 +1871,42 @@ private struct JSONEncoderTests { } } await #expect(processExitsWith: .failure) { - let _ = try JSONEncoder().encode(RedundantEncoding(subcase: .replaceValueWithKeyedContainer)) + let _ = try ReerJSONEncoder().encode(RedundantEncoding(subcase: .replaceValueWithKeyedContainer)) } await #expect(processExitsWith: .failure) { - let _ = try JSONEncoder().encode(RedundantEncoding(subcase: .replaceValueWithUnkeyedContainer)) + let _ = try ReerJSONEncoder().encode(RedundantEncoding(subcase: .replaceValueWithUnkeyedContainer)) } await #expect(processExitsWith: .failure) { - let _ = try JSONEncoder().encode(RedundantEncoding(subcase: .replaceKeyedContainerWithUnkeyed)) + let _ = try ReerJSONEncoder().encode(RedundantEncoding(subcase: .replaceKeyedContainerWithUnkeyed)) } await #expect(processExitsWith: .failure) { - let _ = try JSONEncoder().encode(RedundantEncoding(subcase: .replaceUnkeyedContainerWithKeyed)) + let _ = try ReerJSONEncoder().encode(RedundantEncoding(subcase: .replaceUnkeyedContainerWithKeyed)) } } #endif @Test func decodeIfPresent() throws { - let emptyDictJSON = try JSONEncoder().encode(DecodeIfPresentAllTypes.allNils) + let emptyDictJSON = try ReerJSONEncoder().encode(DecodeIfPresentAllTypes.allNils) let testEmptyDict = try JSONDecoder().decode(DecodeIfPresentAllTypes.self, from: emptyDictJSON) #expect(testEmptyDict == .allNils) - let allNullDictJSON = try JSONEncoder().encode(DecodeIfPresentAllTypes.allNils) + let allNullDictJSON = try ReerJSONEncoder().encode(DecodeIfPresentAllTypes.allNils) let testAllNullDict = try JSONDecoder().decode(DecodeIfPresentAllTypes.self, from: allNullDictJSON) #expect(testAllNullDict == .allNils) - let allOnesDictJSON = try JSONEncoder().encode(DecodeIfPresentAllTypes.allOnes) + let allOnesDictJSON = try ReerJSONEncoder().encode(DecodeIfPresentAllTypes.allOnes) let testAllOnesDict = try JSONDecoder().decode(DecodeIfPresentAllTypes.self, from: allOnesDictJSON) #expect(testAllOnesDict == .allOnes) - let emptyArrayJSON = try JSONEncoder().encode(DecodeIfPresentAllTypes.allNils) + let emptyArrayJSON = try ReerJSONEncoder().encode(DecodeIfPresentAllTypes.allNils) let testEmptyArray = try JSONDecoder().decode(DecodeIfPresentAllTypes.self, from: emptyArrayJSON) #expect(testEmptyArray == .allNils) - let allNullArrayJSON = try JSONEncoder().encode(DecodeIfPresentAllTypes.allNils) + let allNullArrayJSON = try ReerJSONEncoder().encode(DecodeIfPresentAllTypes.allNils) let testAllNullArray = try JSONDecoder().decode(DecodeIfPresentAllTypes.self, from: allNullArrayJSON) #expect(testAllNullArray == .allNils) - let allOnesArrayJSON = try JSONEncoder().encode(DecodeIfPresentAllTypes.allOnes) + let allOnesArrayJSON = try ReerJSONEncoder().encode(DecodeIfPresentAllTypes.allOnes) let testAllOnesArray = try JSONDecoder().decode(DecodeIfPresentAllTypes.self, from: allOnesArrayJSON) #expect(testAllOnesArray == .allOnes) } @@ -2376,7 +2376,7 @@ extension JSONEncoderTests { let expected = "{\"leaveMeAlone\":\"test\"}" let toEncode: [String: String] = ["leaveMeAlone": "test"] - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase let resultData = try encoder.encode(toEncode) let resultString = String(bytes: resultData, encoding: .utf8) @@ -2406,7 +2406,7 @@ extension JSONEncoderTests { // Encoding let encoded = DecodeMe4(thisIsCamelCase: "test", thisIsCamelCaseToo: "test2") - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase let encodingResultData = try encoder.encode(encoded) let encodingResultString = try #require(String(bytes: encodingResultData, encoding: .utf8)) @@ -2433,7 +2433,7 @@ extension JSONEncoderTests { @Test func decodingKeyStrategyCamelGenerated() throws { let encoded = DecodeMe3(thisIsCamelCase: "test") - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase let resultData = try encoder.encode(encoded) let resultString = String(bytes: resultData, encoding: .utf8) @@ -2466,7 +2466,7 @@ extension JSONEncoderTests { @Test func encodingDictionaryFailureKeyPath() { let toEncode: [String: EncodeFailure] = ["key": EncodeFailure(someValue: Double.nan)] - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase do { _ = try encoder.encode(toEncode) @@ -2482,7 +2482,7 @@ extension JSONEncoderTests { @Test func encodingDictionaryFailureKeyPathNested() { let toEncode: [String: [String: EncodeFailureNested]] = ["key": ["sub_key": EncodeFailureNested(nestedValue: EncodeFailure(someValue: Double.nan))]] - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase do { _ = try encoder.encode(toEncode) @@ -2534,7 +2534,7 @@ extension JSONEncoderTests { let expected = "{\"\(test.1)\":\"test\"}" let encoded = EncodeMe(keyName: test.0) - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase let resultData = try encoder.encode(encoded) let resultString = String(bytes: resultData, encoding: .utf8) @@ -2563,10 +2563,10 @@ extension JSONEncoderTests { return } - let prettyPrintEncoder = JSONEncoder() + let prettyPrintEncoder = ReerJSONEncoder() prettyPrintEncoder.outputFormatting = .prettyPrinted - for encoder in [JSONEncoder(), prettyPrintEncoder] { + for encoder in [ReerJSONEncoder(), prettyPrintEncoder] { #expect(throws: Never.self, sourceLocation: sourceLocation) { let reencodedData = try encoder.encode(decoded) let redecodedObjects = try decoder.decode(T.self, from: reencodedData) @@ -3117,7 +3117,7 @@ extension JSONEncoderTests { } } - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.outputFormatting = .sortedKeys let data = try encoder.encode(EncodesTwice()) let string = String(data: data, encoding: .utf8)! @@ -3139,7 +3139,7 @@ extension JSONEncoderTests { try keyed.encode("c", forKey: .a) } } - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.outputFormatting = .sortedKeys let data = try encoder.encode(Test()) let string = String(data: data, encoding: .utf8)! From 6fa058adb540f95ffd5987b038fcefdbda2402e5 Mon Sep 17 00:00:00 2001 From: phoenix Date: Mon, 13 Apr 2026 21:12:07 +0800 Subject: [PATCH 2/7] Optimize: replace post-processing with in-place sort, adopt yyjson native 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 --- Sources/ReerJSON/ReerJSONEncoder.swift | 137 ++++----------------- Tests/ReerJSONTests/JSONEncoderTests.swift | 4 +- 2 files changed, 27 insertions(+), 114 deletions(-) diff --git a/Sources/ReerJSON/ReerJSONEncoder.swift b/Sources/ReerJSON/ReerJSONEncoder.swift index 3cf1e4c..a01d8a4 100644 --- a/Sources/ReerJSON/ReerJSONEncoder.swift +++ b/Sources/ReerJSON/ReerJSONEncoder.swift @@ -197,17 +197,15 @@ open class ReerJSONEncoder { try value.encode(to: encoder) } - guard var root = encoder.takeValue() else { + guard let root = encoder.takeValue() else { throw EncodingError.invalidValue(value, EncodingError.Context( codingPath: [], debugDescription: "Top-level \(T.self) did not encode any values." )) } - let sortKeys = outputFormatting.contains(.sortedKeys) - - if sortKeys { - root = sortMutVal(root, doc: doc) + if outputFormatting.contains(.sortedKeys) { + sortObjectKeys(root) } yyjson_mut_doc_set_root(doc, root) @@ -232,126 +230,41 @@ open class ReerJSONEncoder { )) } defer { free(cstr) } - lowercaseUnicodeEscapes(cstr, len) - - if outputFormatting.contains(.prettyPrinted) { - var data = Data(bytes: cstr, count: len) - addSpaceBeforeColonInPrettyJSON(&data) - return data - } return Data(bytes: cstr, count: len) } - private func addSpaceBeforeColonInPrettyJSON(_ data: inout Data) { - // yyjson outputs "key": value but Foundation outputs "key" : value - var result = Data() - result.reserveCapacity(data.count + data.count / 10) - - var i = 0 - let bytes = Array(data) - let count = bytes.count - var inString = false - var escaped = false - - while i < count { - let byte = bytes[i] - - if escaped { - result.append(byte) - escaped = false - i += 1 - continue - } - - if byte == 0x5C /* \ */ && inString { - result.append(byte) - escaped = true - i += 1 - continue - } - - if byte == 0x22 /* " */ { - inString = !inString - result.append(byte) - i += 1 - continue - } - - if !inString && byte == 0x3A /* : */ && i > 0 && bytes[i-1] == 0x22 /* " */ { - result.append(0x20) // space - result.append(byte) // : - i += 1 - continue - } - - result.append(byte) - i += 1 - } - - data = result - } + // MARK: - In-place sort (no tree rebuild, no extra allocation) - private func lowercaseUnicodeEscapes(_ buf: UnsafeMutablePointer, _ len: Int) { - let ptr = UnsafeMutableRawPointer(buf).assumingMemoryBound(to: UInt8.self) - var i = 0 - while i < len - 5 { - if ptr[i] == 0x5C /* \ */ && ptr[i+1] == 0x75 /* u */ { - for j in (i+2)...(i+5) { - let c = ptr[j] - if c >= 0x41 && c <= 0x46 { // A-F - ptr[j] = c + 32 // a-f - } - } - i += 6 - } else { - i += 1 - } - } - } - - private func sortMutVal(_ val: UnsafeMutablePointer, doc: UnsafeMutablePointer) -> UnsafeMutablePointer { + private func sortObjectKeys(_ val: UnsafeMutablePointer) { + typealias MutVal = UnsafeMutablePointer + if yyjson_mut_is_obj(val) { - let newObj = yyjson_mut_obj(doc)! - - var pairs: [(key: String, keyVal: UnsafeMutablePointer, valVal: UnsafeMutablePointer)] = [] - + var pairs: [(keyVal: MutVal, val: MutVal, keyStr: UnsafePointer)] = [] + pairs.reserveCapacity(Int(yyjson_mut_obj_size(val))) + var iter = yyjson_mut_obj_iter() - yyjson_mut_obj_iter_init(val, &iter) + guard yyjson_mut_obj_iter_init(val, &iter) else { return } + while let keyPtr = yyjson_mut_obj_iter_next(&iter) { - guard let valPtr = yyjson_mut_obj_iter_get_val(keyPtr) else { continue } - let keyStr: String - if let cStr = yyjson_mut_get_str(keyPtr) { - keyStr = String(cString: cStr) - } else { - keyStr = "" - } - pairs.append((key: keyStr, keyVal: keyPtr, valVal: valPtr)) - } - - pairs.sort { a, b in - a.key.utf8.lexicographicallyPrecedes(b.key.utf8) + guard let valPtr = yyjson_mut_obj_iter_get_val(keyPtr), + let keyStr = yyjson_mut_get_str(keyPtr) else { continue } + pairs.append((keyPtr, valPtr, keyStr)) } - + + pairs.sort { strcmp($0.keyStr, $1.keyStr) < 0 } + + yyjson_mut_obj_clear(val) + for pair in pairs { - let sortedVal = sortMutVal(pair.valVal, doc: doc) - let newKey = yyjson_mut_strcpy(doc, pair.key)! - yyjson_mut_obj_add(newObj, newKey, sortedVal) + sortObjectKeys(pair.val) + yyjson_mut_obj_add(val, pair.keyVal, pair.val) } - - return newObj } else if yyjson_mut_is_arr(val) { - let newArr = yyjson_mut_arr(doc)! - var iter = yyjson_mut_arr_iter() - yyjson_mut_arr_iter_init(val, &iter) - while let elemPtr = yyjson_mut_arr_iter_next(&iter) { - let sortedElem = sortMutVal(elemPtr, doc: doc) - yyjson_mut_arr_append(newArr, sortedElem) + guard yyjson_mut_arr_iter_init(val, &iter) else { return } + while let elem = yyjson_mut_arr_iter_next(&iter) { + sortObjectKeys(elem) } - - return newArr } - - return val } } diff --git a/Tests/ReerJSONTests/JSONEncoderTests.swift b/Tests/ReerJSONTests/JSONEncoderTests.swift index 69377a4..919632d 100644 --- a/Tests/ReerJSONTests/JSONEncoderTests.swift +++ b/Tests/ReerJSONTests/JSONEncoderTests.swift @@ -1153,7 +1153,7 @@ private struct JSONEncoderTests { @Test func encodingJSONHexUnicodeEscapes() throws { let testCases = [ "\u{0001}\u{0002}\u{0003}": "\"\\u0001\\u0002\\u0003\"", - "\u{0010}\u{0018}\u{001f}": "\"\\u0010\\u0018\\u001f\"", + "\u{0010}\u{0018}\u{001f}": "\"\\u0010\\u0018\\u001F\"", ] for (string, json) in testCases { _testRoundTrip(of: string, expectedJSON: Data(json.utf8)) @@ -2979,7 +2979,7 @@ extension JSONEncoderTests { } @Test func encodingOutputFormattingPrettyPrintedSortedKeys() { - let expectedJSON = "{\n \"email\" : \"appleseed@apple.com\",\n \"name\" : \"Johnny Appleseed\"\n}".data(using: .utf8)! + let expectedJSON = "{\n \"email\": \"appleseed@apple.com\",\n \"name\": \"Johnny Appleseed\"\n}".data(using: .utf8)! let person = Person.testValue _testRoundTrip(of: person, expectedJSON: expectedJSON, outputFormatting: [.prettyPrinted, .sortedKeys]) } From 535eb37723118a307592a22002691ab0fd48aab4 Mon Sep 17 00:00:00 2001 From: phoenix Date: Mon, 13 Apr 2026 21:14:42 +0800 Subject: [PATCH 3/7] Update README: add ReerJSONEncoder usage and Encoder diff table Made-with: Cursor --- README.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8e432fb..b04e7a0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # ReerJSON -## A faster version of JSONDecoder based on [yyjson](https://github.com/ibireme/yyjson) +## A faster version of JSONDecoder & JSONEncoder based on [yyjson](https://github.com/ibireme/yyjson) ![Coverage: 88%](https://img.shields.io/static/v1?label=coverage&message=88%&color=brightgreen) [![SwiftPM compatible](https://img.shields.io/badge/SwiftPM-compatible%20%28iOS%29-brightgreen)](https://swift.org/package-manager/) @@ -47,7 +47,7 @@ Depend on `ReerJSON` in your target. ``` # Usage -`ReerJSONDecoder` is API-compatible replacements for Foundation's JSONDecoder. +`ReerJSONDecoder` and `ReerJSONEncoder` are API-compatible replacements for Foundation's JSONDecoder and JSONEncoder. Simply swap the type and add the import, no other code changes required: ``` @@ -55,9 +55,11 @@ import ReerJSON // Before let decoder = JSONDecoder() +let encoder = JSONEncoder() // After let decoder = ReerJSONDecoder() +let encoder = ReerJSONEncoder() ``` All public interfaces, behaviors, error types, and coding strategies are identical to the Foundation counterparts. The ReerJSON test suite includes exhaustive test cases covering every feature, ensuring full compatibility. @@ -67,17 +69,26 @@ All public interfaces, behaviors, error types, and coding strategies are identic Except for the items listed below, ReerJSON behaves exactly the same as Foundation—every capability, every thrown error, and every edge case is covered by a comprehensive test suite. +## Decoder + | Decoder Diff | Foundation |ReerJSON | |---------------------------|------------|---------------------------| | JSON5 | ✅ | ✅ | | assumesTopLevelDictionary | ✅ | ❌ | | Infinity and NaN | ±Infinity, ±NaN | ±Infinity, ±NaN, ±Inf and case-insensitive. See [details](https://github.com/reers/ReerJSON/blob/main/Tests/ReerJSONTests/JSONEncoderTests.swift#L1975) | +## Encoder + +| 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) | + # TODO * [x] Add GitHub workflow for CI. * [x] Support `CodableWithConfiguration`. * [x] Support JSON5 decoding. -* [ ] Implement ReerJSONEncoder. +* [x] Implement ReerJSONEncoder. # License This project is licensed under the MIT License. From fa322f7740b467f97a078ac94a11ed90c9a8b85a Mon Sep 17 00:00:00 2001 From: phoenix Date: Mon, 13 Apr 2026 21:50:11 +0800 Subject: [PATCH 4/7] Optimize encoder performance: T.self== dispatch, bulk yyjson array APIs, nested double fast paths - Replace `as?` type checks with `T.self ==` in generic encode contexts (decoder pattern) - Add wrapGenericEncodable 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 --- Sources/ReerJSON/JSONEncoderImpl.swift | 657 +++++++++++-------------- Sources/ReerJSON/Utilities.swift | 8 + 2 files changed, 292 insertions(+), 373 deletions(-) diff --git a/Sources/ReerJSON/JSONEncoderImpl.swift b/Sources/ReerJSON/JSONEncoderImpl.swift index e98db3b..e9b0714 100644 --- a/Sources/ReerJSON/JSONEncoderImpl.swift +++ b/Sources/ReerJSON/JSONEncoderImpl.swift @@ -36,333 +36,351 @@ class JSONEncoderImpl: Encoder { let doc: UnsafeMutablePointer let options: ReerJSONEncoder.Options var codingPath: [CodingKey] - - var userInfo: [CodingUserInfoKey: Any] { - return options.userInfo - } - + + var userInfo: [CodingUserInfoKey: Any] { options.userInfo } + var singleValue: UnsafeMutablePointer? var array: UnsafeMutablePointer? var object: UnsafeMutablePointer? - + init(doc: UnsafeMutablePointer, codingPath: [CodingKey], options: ReerJSONEncoder.Options) { self.doc = doc self.codingPath = codingPath self.options = options } - + + @inline(__always) func takeValue() -> UnsafeMutablePointer? { - if let object { - return object - } - if let array { - return array - } + if let object { return object } + if let array { return array } return singleValue } - + // MARK: - Encoder Methods - + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { if let object { - let container = YYJSONKeyedEncodingContainer(impl: self, codingPath: codingPath, object: object) - return KeyedEncodingContainer(container) + return KeyedEncodingContainer(YYJSONKeyedEncodingContainer(impl: self, codingPath: codingPath, object: object)) } - if let sv = singleValue, yyjson_mut_is_obj(sv) { - self.object = sv - self.singleValue = nil - let container = YYJSONKeyedEncodingContainer(impl: self, codingPath: codingPath, object: sv) - return KeyedEncodingContainer(container) + self.object = sv; self.singleValue = nil + return KeyedEncodingContainer(YYJSONKeyedEncodingContainer(impl: self, codingPath: codingPath, object: sv)) } - guard singleValue == nil, array == nil else { preconditionFailure("Attempt to push new keyed encoding container when already previously encoded at this path.") } - let obj = yyjson_mut_obj(doc)! self.object = obj - let container = YYJSONKeyedEncodingContainer(impl: self, codingPath: codingPath, object: obj) - return KeyedEncodingContainer(container) + return KeyedEncodingContainer(YYJSONKeyedEncodingContainer(impl: self, codingPath: codingPath, object: obj)) } - + func unkeyedContainer() -> UnkeyedEncodingContainer { - if let array { - return YYJSONUnkeyedEncodingContainer(impl: self, codingPath: codingPath, array: array) - } - + if let array { return YYJSONUnkeyedEncodingContainer(impl: self, codingPath: codingPath, array: array) } if let sv = singleValue, yyjson_mut_is_arr(sv) { - self.array = sv - self.singleValue = nil + self.array = sv; self.singleValue = nil return YYJSONUnkeyedEncodingContainer(impl: self, codingPath: codingPath, array: sv) } - guard singleValue == nil, object == nil else { preconditionFailure("Attempt to push new unkeyed encoding container when already previously encoded at this path.") } - let arr = yyjson_mut_arr(doc)! self.array = arr return YYJSONUnkeyedEncodingContainer(impl: self, codingPath: codingPath, array: arr) } - - func singleValueContainer() -> SingleValueEncodingContainer { - return self - } - - // MARK: - Value Creation Helpers - - @inline(__always) - func wrapInt(_ value: some FixedWidthInteger & SignedInteger) -> UnsafeMutablePointer { - return yyjson_mut_sint(doc, Int64(value)) - } - - @inline(__always) - func wrapUInt(_ value: some FixedWidthInteger & UnsignedInteger) -> UnsafeMutablePointer { - return yyjson_mut_uint(doc, UInt64(value)) - } - + + func singleValueContainer() -> SingleValueEncodingContainer { self } + + // MARK: - Primitive Value Helpers + + @inline(__always) func wrapInt(_ v: some FixedWidthInteger & SignedInteger) -> UnsafeMutablePointer { yyjson_mut_sint(doc, Int64(v)) } + @inline(__always) func wrapUInt(_ v: some FixedWidthInteger & UnsignedInteger) -> UnsafeMutablePointer { yyjson_mut_uint(doc, UInt64(v)) } + #if compiler(>=6.0) @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) - func wrapInt128(_ value: Int128) -> UnsafeMutablePointer { - let str = String(value) - return yyjson_mut_rawcpy(doc, str) - } - + @inline(__always) func wrapInt128(_ v: Int128) -> UnsafeMutablePointer { yyjson_mut_rawcpy(doc, String(v)) } @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) - func wrapUInt128(_ value: UInt128) -> UnsafeMutablePointer { - let str = String(value) - return yyjson_mut_rawcpy(doc, str) - } + @inline(__always) func wrapUInt128(_ v: UInt128) -> UnsafeMutablePointer { yyjson_mut_rawcpy(doc, String(v)) } #endif - + @inline(__always) - func wrapDouble(_ value: Double) -> UnsafeMutablePointer { - let str = formatDouble(value) - return yyjson_mut_rawcpy(doc, str) - } - - private func formatDouble(_ value: Double) -> String { - var string = value.description - if string.hasSuffix(".0") { - string.removeLast(2) - } - return string - } - func wrapFloat(_ float: T, for additionalKey: CodingKey?) throws -> UnsafeMutablePointer { guard !float.isNaN, !float.isInfinite else { - if case .convertToString(let posInfString, let negInfString, let nanString) = options.nonConformingFloatEncodingStrategy { - switch float { - case T.infinity: - return wrapString(posInfString) - case -T.infinity: - return wrapString(negInfString) - default: - return wrapString(nanString) - } - } - - var path = codingPath - if let additionalKey { path.append(additionalKey) } - throw EncodingError.invalidValue(float, .init( - codingPath: path, - debugDescription: "Unable to encode \(T.self).\(float) directly in JSON." - )) + return try wrapNonConformingFloat(float, for: additionalKey) + } + let d = Double(float) + if T.self == Double.self && d != d.rounded(.towardZero) { + return yyjson_mut_double(doc, d) } - - var string = float.description - if string.hasSuffix(".0") { - string.removeLast(2) + var s = float.description + if s.hasSuffix(".0") { s.removeLast(2) } + return yyjson_mut_rawcpy(doc, s) + } + + @inline(never) + private func wrapNonConformingFloat(_ float: T, for additionalKey: CodingKey?) throws -> UnsafeMutablePointer { + if case .convertToString(let posInf, let negInf, let nan) = options.nonConformingFloatEncodingStrategy { + switch float { + case T.infinity: return wrapString(posInf) + case -T.infinity: return wrapString(negInf) + default: return wrapString(nan) + } } - return yyjson_mut_rawcpy(doc, string) + var path = codingPath + if let additionalKey { path.append(additionalKey) } + throw EncodingError.invalidValue(float, .init(codingPath: path, debugDescription: "Unable to encode \(T.self).\(float) directly in JSON.")) } - + @inline(__always) func wrapString(_ string: String) -> UnsafeMutablePointer { - return string.withCString { cStr in - let len = string.utf8.count - return yyjson_mut_strncpy(doc, cStr, len) + var s = string + return s.withUTF8 { buf in + buf.baseAddress!.withMemoryRebound(to: CChar.self, capacity: buf.count) { ptr in + yyjson_mut_strncpy(doc, ptr, buf.count) + } + } + } + + // MARK: - Generic Encodable wrapping (T.self == for fast dispatch) + + func wrapGenericEncodable(_ value: T, for additionalKey: CodingKey?) throws -> UnsafeMutablePointer? { + if T.self == Date.self { return try wrapDateValue(value as! Date, for: additionalKey) } + if T.self == Data.self { return try wrapDataValue(value as! Data, for: additionalKey) } + if T.self == URL.self { return wrapString((value as! URL).absoluteString) } + if T.self == Decimal.self { return yyjson_mut_rawcpy(doc, (value as! Decimal).description) } + + #if compiler(>=6.0) + if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { + if T.self == Int128.self { return wrapInt128(value as! Int128) } + if T.self == UInt128.self { return wrapUInt128(value as! UInt128) } + } + #endif + + if T.self == [Double].self { return wrapBulkDoubleArray(value as! [Double]) } + if T.self == [[Double]].self { return wrapNestedDoubleArray(value as! [[Double]]) } + if T.self == [[[Double]]].self { return wrapTripleNestedDoubleArray(value as! [[[Double]]]) } + if T.self == [String].self { return wrapBulkStringArray(value as! [String]) } + if T.self == [Bool].self { return wrapBulkBoolArray(value as! [Bool]) } + if T.self == [Int].self { return wrapBulkIntArray(value as! [Int]) } + if T.self == [Int8].self { return wrapBulkInt8Array(value as! [Int8]) } + if T.self == [Int16].self { return wrapBulkInt16Array(value as! [Int16]) } + if T.self == [Int32].self { return wrapBulkInt32Array(value as! [Int32]) } + if T.self == [Int64].self { return wrapBulkInt64Array(value as! [Int64]) } + if T.self == [UInt].self { return wrapBulkUIntArray(value as! [UInt]) } + if T.self == [UInt8].self { return wrapBulkUInt8Array(value as! [UInt8]) } + if T.self == [UInt16].self { return wrapBulkUInt16Array(value as! [UInt16]) } + if T.self == [UInt32].self { return wrapBulkUInt32Array(value as! [UInt32]) } + if T.self == [UInt64].self { return wrapBulkUInt64Array(value as! [UInt64]) } + if T.self == [Float].self { return try wrapFloatArray(value as! [Float]) } + + if T.self is _JSONStringDictionaryEncodableMarker.Type, let dict = value as? [String: Encodable] { + return try wrapStringKeyedDictValue(dict, for: additionalKey) } + + let newPath = codingPath + (additionalKey.map { [$0] } ?? []) + let sub = JSONEncoderImpl(doc: doc, codingPath: newPath, options: options) + try value.encode(to: sub) + return sub.takeValue() } - + func wrapEncodable(_ value: Encodable, for additionalKey: CodingKey?) throws -> UnsafeMutablePointer? { - if let date = value as? Date { - return try wrapDateValue(date, for: additionalKey) - } else if let data = value as? Data { - return try wrapDataValue(data, for: additionalKey) - } else if let url = value as? URL { - return wrapString(url.absoluteString) - } else if let decimal = value as? Decimal { - return yyjson_mut_rawcpy(doc, decimal.description) - } else if value is _JSONStringDictionaryEncodableMarker, let dict = value as? [String: Encodable] { + if let date = value as? Date { return try wrapDateValue(date, for: additionalKey) } + if let data = value as? Data { return try wrapDataValue(data, for: additionalKey) } + if let url = value as? URL { return wrapString(url.absoluteString) } + if let decimal = value as? Decimal { return yyjson_mut_rawcpy(doc, decimal.description) } + if value is _JSONStringDictionaryEncodableMarker, let dict = value as? [String: Encodable] { return try wrapStringKeyedDictValue(dict, for: additionalKey) } #if compiler(>=6.0) if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { - if let i128 = value as? Int128 { - return wrapInt128(i128) - } - if let u128 = value as? UInt128 { - return wrapUInt128(u128) - } + if let i128 = value as? Int128 { return wrapInt128(i128) } + if let u128 = value as? UInt128 { return wrapUInt128(u128) } } #endif - + let newPath = codingPath + (additionalKey.map { [$0] } ?? []) - let subEncoder = JSONEncoderImpl(doc: doc, codingPath: newPath, options: options) - try value.encode(to: subEncoder) - return subEncoder.takeValue() + let sub = JSONEncoderImpl(doc: doc, codingPath: newPath, options: options) + try value.encode(to: sub) + return sub.takeValue() + } + + // MARK: - Bulk array fast paths (single C call, zero Swift loop) + + @inline(__always) private func wrapBulkDoubleArray(_ arr: [Double]) -> UnsafeMutablePointer { + if options.nonConformingFloatEncodingStrategy.isThrow { + return arr.withUnsafeBufferPointer { yyjson_mut_arr_with_double(doc, $0.baseAddress, $0.count) } + } + let result = yyjson_mut_arr(doc)! + for d in arr { + if d != d.rounded(.towardZero) && !d.isNaN && !d.isInfinite { + yyjson_mut_arr_add_real(doc, result, d) + } else { + yyjson_mut_arr_append(result, (try? wrapFloat(d, for: nil)) ?? yyjson_mut_null(doc)) + } + } + return result + } + @inline(__always) private func wrapBulkBoolArray(_ arr: [Bool]) -> UnsafeMutablePointer { + arr.withUnsafeBufferPointer { yyjson_mut_arr_with_bool(doc, $0.baseAddress, $0.count) } + } + @inline(__always) private func wrapBulkInt8Array(_ arr: [Int8]) -> UnsafeMutablePointer { + arr.withUnsafeBufferPointer { yyjson_mut_arr_with_sint8(doc, $0.baseAddress, $0.count) } + } + @inline(__always) private func wrapBulkInt16Array(_ arr: [Int16]) -> UnsafeMutablePointer { + arr.withUnsafeBufferPointer { yyjson_mut_arr_with_sint16(doc, $0.baseAddress, $0.count) } + } + @inline(__always) private func wrapBulkInt32Array(_ arr: [Int32]) -> UnsafeMutablePointer { + arr.withUnsafeBufferPointer { yyjson_mut_arr_with_sint32(doc, $0.baseAddress, $0.count) } + } + @inline(__always) private func wrapBulkInt64Array(_ arr: [Int64]) -> UnsafeMutablePointer { + arr.withUnsafeBufferPointer { yyjson_mut_arr_with_sint64(doc, $0.baseAddress, $0.count) } + } + @inline(__always) private func wrapBulkIntArray(_ arr: [Int]) -> UnsafeMutablePointer { + let mapped = arr.map { Int64($0) } + return mapped.withUnsafeBufferPointer { yyjson_mut_arr_with_sint64(doc, $0.baseAddress, $0.count) } + } + @inline(__always) private func wrapBulkUInt8Array(_ arr: [UInt8]) -> UnsafeMutablePointer { + arr.withUnsafeBufferPointer { yyjson_mut_arr_with_uint8(doc, $0.baseAddress, $0.count) } + } + @inline(__always) private func wrapBulkUInt16Array(_ arr: [UInt16]) -> UnsafeMutablePointer { + arr.withUnsafeBufferPointer { yyjson_mut_arr_with_uint16(doc, $0.baseAddress, $0.count) } + } + @inline(__always) private func wrapBulkUInt32Array(_ arr: [UInt32]) -> UnsafeMutablePointer { + arr.withUnsafeBufferPointer { yyjson_mut_arr_with_uint32(doc, $0.baseAddress, $0.count) } + } + @inline(__always) private func wrapBulkUInt64Array(_ arr: [UInt64]) -> UnsafeMutablePointer { + arr.withUnsafeBufferPointer { yyjson_mut_arr_with_uint64(doc, $0.baseAddress, $0.count) } } - + @inline(__always) private func wrapBulkUIntArray(_ arr: [UInt]) -> UnsafeMutablePointer { + let mapped = arr.map { UInt64($0) } + return mapped.withUnsafeBufferPointer { yyjson_mut_arr_with_uint64(doc, $0.baseAddress, $0.count) } + } + @inline(__always) private func wrapBulkStringArray(_ arr: [String]) -> UnsafeMutablePointer { + let result = yyjson_mut_arr(doc)! + for s in arr { yyjson_mut_arr_append(result, wrapString(s)) } + return result + } + @inline(__always) private func wrapFloatArray(_ arr: [Float]) throws -> UnsafeMutablePointer { + let result = yyjson_mut_arr(doc)! + for f in arr { yyjson_mut_arr_append(result, try wrapFloat(f, for: nil)) } + return result + } + + // MARK: - Nested double array fast paths (Canada-killer) + + private func wrapNestedDoubleArray(_ arr: [[Double]]) -> UnsafeMutablePointer { + let result = yyjson_mut_arr(doc)! + for inner in arr { yyjson_mut_arr_append(result, wrapBulkDoubleArray(inner)) } + return result + } + + private func wrapTripleNestedDoubleArray(_ arr: [[[Double]]]) -> UnsafeMutablePointer { + let result = yyjson_mut_arr(doc)! + for inner in arr { yyjson_mut_arr_append(result, wrapNestedDoubleArray(inner)) } + return result + } + + // MARK: - Date / Data / Dict wrapping + func wrapDateValue(_ date: Date, for additionalKey: CodingKey?) throws -> UnsafeMutablePointer? { switch options.dateEncodingStrategy { case .deferredToDate: - let newPath = codingPath + (additionalKey.map { [$0] } ?? []) - let subEncoder = JSONEncoderImpl(doc: doc, codingPath: newPath, options: options) - try date.encode(to: subEncoder) - return subEncoder.takeValue() - + let sub = JSONEncoderImpl(doc: doc, codingPath: codingPath + (additionalKey.map { [$0] } ?? []), options: options) + try date.encode(to: sub) + return sub.takeValue() case .secondsSince1970: return try wrapFloat(date.timeIntervalSince1970, for: additionalKey) - case .millisecondsSince1970: return try wrapFloat(1000.0 * date.timeIntervalSince1970, for: additionalKey) - case .iso8601: - let string = _iso8601Formatter.string(from: date) - return wrapString(string) - + return wrapString(_iso8601Formatter.string(from: date)) case .formatted(let formatter): - let string = formatter.string(from: date) - return wrapString(string) - + return wrapString(formatter.string(from: date)) case .custom(let closure): - let newPath = codingPath + (additionalKey.map { [$0] } ?? []) - let subEncoder = JSONEncoderImpl(doc: doc, codingPath: newPath, options: options) - try closure(date, subEncoder) - return subEncoder.takeValue() ?? yyjson_mut_obj(doc) - - @unknown default: - fatalError() + let sub = JSONEncoderImpl(doc: doc, codingPath: codingPath + (additionalKey.map { [$0] } ?? []), options: options) + try closure(date, sub) + return sub.takeValue() ?? yyjson_mut_obj(doc) + @unknown default: fatalError() } } - + func wrapDataValue(_ data: Data, for additionalKey: CodingKey?) throws -> UnsafeMutablePointer? { switch options.dataEncodingStrategy { case .deferredToData: - let newPath = codingPath + (additionalKey.map { [$0] } ?? []) - let subEncoder = JSONEncoderImpl(doc: doc, codingPath: newPath, options: options) - try data.encode(to: subEncoder) - return subEncoder.takeValue() - + let sub = JSONEncoderImpl(doc: doc, codingPath: codingPath + (additionalKey.map { [$0] } ?? []), options: options) + try data.encode(to: sub) + return sub.takeValue() case .base64: - let base64 = data.base64EncodedString() - return wrapString(base64) - + return wrapString(data.base64EncodedString()) case .custom(let closure): - let newPath = codingPath + (additionalKey.map { [$0] } ?? []) - let subEncoder = JSONEncoderImpl(doc: doc, codingPath: newPath, options: options) - try closure(data, subEncoder) - return subEncoder.takeValue() ?? yyjson_mut_obj(doc) - - @unknown default: - fatalError() + let sub = JSONEncoderImpl(doc: doc, codingPath: codingPath + (additionalKey.map { [$0] } ?? []), options: options) + try closure(data, sub) + return sub.takeValue() ?? yyjson_mut_obj(doc) + @unknown default: fatalError() } } - + func wrapStringKeyedDictValue(_ dict: [String: Encodable], for additionalKey: CodingKey?) throws -> UnsafeMutablePointer? { let obj = yyjson_mut_obj(doc)! - let savedCodingPath = codingPath - if let additionalKey { - codingPath.append(additionalKey) - } + let savedPath = codingPath + if let additionalKey { codingPath.append(additionalKey) } for (key, value) in dict { let keyVal = wrapString(key) - let dictKey = _CodingKey(stringValue: key)! - codingPath.append(dictKey) - let subEncoder = JSONEncoderImpl(doc: doc, codingPath: codingPath, options: options) - let val = try subEncoder.wrapEncodable(value, for: nil) ?? yyjson_mut_obj(doc)! + codingPath.append(_CodingKey(stringValue: key)!) + let sub = JSONEncoderImpl(doc: doc, codingPath: codingPath, options: options) + let val = try sub.wrapEncodable(value, for: nil) ?? yyjson_mut_obj(doc)! yyjson_mut_obj_add(obj, keyVal, val) codingPath.removeLast() } - codingPath = savedCodingPath + codingPath = savedPath return obj } - + // MARK: - Key encoding strategy - + + @inline(__always) func convertedKey(_ key: CodingKey) -> String { switch options.keyEncodingStrategy { - case .useDefaultKeys: - return key.stringValue - case .convertToSnakeCase: - return Self._convertToSnakeCase(key.stringValue) - case .custom(let converter): - return converter(codingPath + [key]).stringValue - @unknown default: - return key.stringValue + case .useDefaultKeys: return key.stringValue + case .convertToSnakeCase: return Self._convertToSnakeCase(key.stringValue) + case .custom(let converter): return converter(codingPath + [key]).stringValue + @unknown default: return key.stringValue } } - + static func _convertToSnakeCase(_ stringKey: String) -> String { guard !stringKey.isEmpty else { return stringKey } - var words: [Range] = [] var wordStart = stringKey.startIndex var searchRange = stringKey.index(after: wordStart)..=6.0) @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) - func encode(_ value: Int128) throws { - singleValue = wrapInt128(value) - } - + func encode(_ value: Int128) throws { singleValue = wrapInt128(value) } @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) - func encode(_ value: UInt128) throws { - singleValue = wrapUInt128(value) - } + func encode(_ value: UInt128) throws { singleValue = wrapUInt128(value) } #endif - func encode(_ value: T) throws { - #if compiler(>=6.0) - if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { - if let i128 = value as? Int128 { - singleValue = wrapInt128(i128) - return - } - if let u128 = value as? UInt128 { - singleValue = wrapUInt128(u128) - return - } - } - #endif - singleValue = try wrapEncodable(value, for: nil) ?? yyjson_mut_obj(doc) + singleValue = try wrapGenericEncodable(value, for: nil) ?? yyjson_mut_obj(doc) } } @@ -407,44 +406,21 @@ extension JSONEncoderImpl: SingleValueEncodingContainer { private struct YYJSONKeyedEncodingContainer: KeyedEncodingContainerProtocol { typealias Key = K - let impl: JSONEncoderImpl var codingPath: [CodingKey] let object: UnsafeMutablePointer - var doc: UnsafeMutablePointer { impl.doc } - - init(impl: JSONEncoderImpl, codingPath: [CodingKey], object: UnsafeMutablePointer) { - self.impl = impl - self.codingPath = codingPath - self.object = object - } - + + @inline(__always) private func addToObject(key: String, value: UnsafeMutablePointer) { - let keyVal = impl.wrapString(key) - yyjson_mut_obj_put(object, keyVal, value) - } - - mutating func encodeNil(forKey key: Key) throws { - addToObject(key: impl.convertedKey(key), value: yyjson_mut_null(doc)) + yyjson_mut_obj_put(object, impl.wrapString(key), value) } - - mutating func encode(_ value: Bool, forKey key: Key) throws { - addToObject(key: impl.convertedKey(key), value: yyjson_mut_bool(doc, value)) - } - - mutating func encode(_ value: String, forKey key: Key) throws { - addToObject(key: impl.convertedKey(key), value: impl.wrapString(value)) - } - - mutating func encode(_ value: Double, forKey key: Key) throws { - addToObject(key: impl.convertedKey(key), value: try impl.wrapFloat(value, for: key)) - } - - mutating func encode(_ value: Float, forKey key: Key) throws { - addToObject(key: impl.convertedKey(key), value: try impl.wrapFloat(value, for: key)) - } - + + mutating func encodeNil(forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: yyjson_mut_null(doc)) } + mutating func encode(_ value: Bool, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: yyjson_mut_bool(doc, value)) } + mutating func encode(_ value: String, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapString(value)) } + mutating func encode(_ value: Double, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: try impl.wrapFloat(value, for: key)) } + mutating func encode(_ value: Float, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: try impl.wrapFloat(value, for: key)) } mutating func encode(_ value: Int, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapInt(value)) } mutating func encode(_ value: Int8, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapInt(value)) } mutating func encode(_ value: Int16, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapInt(value)) } @@ -455,50 +431,38 @@ private struct YYJSONKeyedEncodingContainer: KeyedEncodingContaine mutating func encode(_ value: UInt16, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapUInt(value)) } mutating func encode(_ value: UInt32, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapUInt(value)) } mutating func encode(_ value: UInt64, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapUInt(value)) } - #if compiler(>=6.0) @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) mutating func encode(_ value: Int128, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapInt128(value)) } @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) mutating func encode(_ value: UInt128, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapUInt128(value)) } #endif - + mutating func encode(_ value: T, forKey key: Key) throws { - let convertedKeyStr = impl.convertedKey(key) - let val = try impl.wrapEncodable(value, for: key) ?? yyjson_mut_obj(doc)! - addToObject(key: convertedKeyStr, value: val) + addToObject(key: impl.convertedKey(key), value: try impl.wrapGenericEncodable(value, for: key) ?? yyjson_mut_obj(doc)!) } - + mutating func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer { - let convertedKeyStr = impl.convertedKey(key) - let existingVal = convertedKeyStr.withCString { cStr in - yyjson_mut_obj_getn(object, cStr, convertedKeyStr.utf8.count) + let k = impl.convertedKey(key) + if let existing = k.withCString({ yyjson_mut_obj_getn(object, $0, k.utf8.count) }), yyjson_mut_is_obj(existing) { + return KeyedEncodingContainer(YYJSONKeyedEncodingContainer(impl: impl, codingPath: codingPath + [key], object: existing)) } - if let existingVal, yyjson_mut_is_obj(existingVal) { - let container = YYJSONKeyedEncodingContainer(impl: impl, codingPath: codingPath + [key], object: existingVal) - return KeyedEncodingContainer(container) - } - let nestedObj = yyjson_mut_obj(doc)! - addToObject(key: convertedKeyStr, value: nestedObj) - let container = YYJSONKeyedEncodingContainer(impl: impl, codingPath: codingPath + [key], object: nestedObj) - return KeyedEncodingContainer(container) + let obj = yyjson_mut_obj(doc)! + addToObject(key: k, value: obj) + return KeyedEncodingContainer(YYJSONKeyedEncodingContainer(impl: impl, codingPath: codingPath + [key], object: obj)) } - + mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { - let convertedKeyStr = impl.convertedKey(key) - let nestedArr = yyjson_mut_arr(doc)! - addToObject(key: convertedKeyStr, value: nestedArr) - return YYJSONUnkeyedEncodingContainer(impl: impl, codingPath: codingPath + [key], array: nestedArr) + let arr = yyjson_mut_arr(doc)! + addToObject(key: impl.convertedKey(key), value: arr) + return YYJSONUnkeyedEncodingContainer(impl: impl, codingPath: codingPath + [key], array: arr) } - + mutating func superEncoder() -> Encoder { - let convertedKeyStr = impl.convertedKey(_CodingKey.super) - return YYJSONReferencingEncoder(impl: impl, key: convertedKeyStr, codingPath: codingPath + [_CodingKey.super], object: object) as Encoder + YYJSONReferencingEncoder(impl: impl, key: impl.convertedKey(_CodingKey.super), codingPath: codingPath + [_CodingKey.super], object: object) } - mutating func superEncoder(forKey key: Key) -> Encoder { - let convertedKeyStr = impl.convertedKey(key) - return YYJSONReferencingEncoder(impl: impl, key: convertedKeyStr, codingPath: codingPath + [key], object: object) as Encoder + YYJSONReferencingEncoder(impl: impl, key: impl.convertedKey(key), codingPath: codingPath + [key], object: object) } } @@ -508,42 +472,14 @@ private struct YYJSONUnkeyedEncodingContainer: UnkeyedEncodingContainer { let impl: JSONEncoderImpl var codingPath: [CodingKey] let array: UnsafeMutablePointer - var doc: UnsafeMutablePointer { impl.doc } - - var count: Int { - Int(yyjson_mut_arr_size(array)) - } - - init(impl: JSONEncoderImpl, codingPath: [CodingKey], array: UnsafeMutablePointer) { - self.impl = impl - self.codingPath = codingPath - self.array = array - } - - mutating func encodeNil() throws { - yyjson_mut_arr_add_null(doc, array) - } - - mutating func encode(_ value: Bool) throws { - yyjson_mut_arr_add_bool(doc, array, value) - } - - mutating func encode(_ value: String) throws { - let val = impl.wrapString(value) - yyjson_mut_arr_append(array, val) - } - - mutating func encode(_ value: Double) throws { - let val = try impl.wrapFloat(value, for: _CodingKey(index: count)) - yyjson_mut_arr_append(array, val) - } - - mutating func encode(_ value: Float) throws { - let val = try impl.wrapFloat(value, for: _CodingKey(index: count)) - yyjson_mut_arr_append(array, val) - } - + var count: Int { Int(yyjson_mut_arr_size(array)) } + + mutating func encodeNil() throws { yyjson_mut_arr_add_null(doc, array) } + mutating func encode(_ value: Bool) throws { yyjson_mut_arr_add_bool(doc, array, value) } + mutating func encode(_ value: String) throws { yyjson_mut_arr_append(array, impl.wrapString(value)) } + mutating func encode(_ value: Double) throws { yyjson_mut_arr_append(array, try impl.wrapFloat(value, for: _CodingKey(index: count))) } + mutating func encode(_ value: Float) throws { yyjson_mut_arr_append(array, try impl.wrapFloat(value, for: _CodingKey(index: count))) } mutating func encode(_ value: Int) throws { yyjson_mut_arr_add_sint(doc, array, Int64(value)) } mutating func encode(_ value: Int8) throws { yyjson_mut_arr_add_sint(doc, array, Int64(value)) } mutating func encode(_ value: Int16) throws { yyjson_mut_arr_add_sint(doc, array, Int64(value)) } @@ -554,77 +490,52 @@ private struct YYJSONUnkeyedEncodingContainer: UnkeyedEncodingContainer { mutating func encode(_ value: UInt16) throws { yyjson_mut_arr_add_uint(doc, array, UInt64(value)) } mutating func encode(_ value: UInt32) throws { yyjson_mut_arr_add_uint(doc, array, UInt64(value)) } mutating func encode(_ value: UInt64) throws { yyjson_mut_arr_add_uint(doc, array, UInt64(value)) } - #if compiler(>=6.0) @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) - mutating func encode(_ value: Int128) throws { - let val = impl.wrapInt128(value) - yyjson_mut_arr_append(array, val) - } + mutating func encode(_ value: Int128) throws { yyjson_mut_arr_append(array, impl.wrapInt128(value)) } @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) - mutating func encode(_ value: UInt128) throws { - let val = impl.wrapUInt128(value) - yyjson_mut_arr_append(array, val) - } + mutating func encode(_ value: UInt128) throws { yyjson_mut_arr_append(array, impl.wrapUInt128(value)) } #endif - + mutating func encode(_ value: T) throws { - let val = try impl.wrapEncodable(value, for: _CodingKey(index: count)) ?? yyjson_mut_obj(doc)! - yyjson_mut_arr_append(array, val) + yyjson_mut_arr_append(array, try impl.wrapGenericEncodable(value, for: _CodingKey(index: count)) ?? yyjson_mut_obj(doc)!) } - + mutating func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer { - let nestedObj = yyjson_mut_obj(doc)! - yyjson_mut_arr_append(array, nestedObj) - let container = YYJSONKeyedEncodingContainer(impl: impl, codingPath: codingPath + [_CodingKey(index: count - 1)], object: nestedObj) - return KeyedEncodingContainer(container) + let obj = yyjson_mut_obj(doc)!; yyjson_mut_arr_append(array, obj) + return KeyedEncodingContainer(YYJSONKeyedEncodingContainer(impl: impl, codingPath: codingPath + [_CodingKey(index: count - 1)], object: obj)) } - mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { - let nestedArr = yyjson_mut_arr(doc)! - yyjson_mut_arr_append(array, nestedArr) - return YYJSONUnkeyedEncodingContainer(impl: impl, codingPath: codingPath + [_CodingKey(index: count - 1)], array: nestedArr) + let arr = yyjson_mut_arr(doc)!; yyjson_mut_arr_append(array, arr) + return YYJSONUnkeyedEncodingContainer(impl: impl, codingPath: codingPath + [_CodingKey(index: count - 1)], array: arr) } - mutating func superEncoder() -> Encoder { - let idx = count - return YYJSONReferencingArrayEncoder(impl: impl, codingPath: codingPath + [_CodingKey(index: idx)], array: array, index: idx) + YYJSONReferencingArrayEncoder(impl: impl, codingPath: codingPath + [_CodingKey(index: count)], array: array, index: count) } } -// MARK: - YYJSONReferencingEncoder (for keyed containers) +// MARK: - Referencing Encoders private class YYJSONReferencingEncoder: JSONEncoderImpl { let key: String let referencedObject: UnsafeMutablePointer - init(impl: JSONEncoderImpl, key: String, codingPath: [CodingKey], object: UnsafeMutablePointer) { - self.key = key - self.referencedObject = object + self.key = key; self.referencedObject = object super.init(doc: impl.doc, codingPath: codingPath, options: impl.options) } - deinit { - let value = takeValue() ?? yyjson_mut_obj(doc)! - let keyVal = wrapString(key) - yyjson_mut_obj_put(referencedObject, keyVal, value) + yyjson_mut_obj_put(referencedObject, wrapString(key), takeValue() ?? yyjson_mut_obj(doc)!) } } -// MARK: - YYJSONReferencingArrayEncoder (for unkeyed containers) - private class YYJSONReferencingArrayEncoder: JSONEncoderImpl { let referencedArray: UnsafeMutablePointer let insertIndex: Int - init(impl: JSONEncoderImpl, codingPath: [CodingKey], array: UnsafeMutablePointer, index: Int) { - self.referencedArray = array - self.insertIndex = index + self.referencedArray = array; self.insertIndex = index super.init(doc: impl.doc, codingPath: codingPath, options: impl.options) } - deinit { - let value = takeValue() ?? yyjson_mut_obj(doc)! - yyjson_mut_arr_insert(referencedArray, value, insertIndex) + yyjson_mut_arr_insert(referencedArray, takeValue() ?? yyjson_mut_obj(doc)!, insertIndex) } } diff --git a/Sources/ReerJSON/Utilities.swift b/Sources/ReerJSON/Utilities.swift index 9ee911e..bb405d9 100644 --- a/Sources/ReerJSON/Utilities.swift +++ b/Sources/ReerJSON/Utilities.swift @@ -142,6 +142,14 @@ extension JSONDecoder.KeyDecodingStrategy { } } +extension JSONEncoder.NonConformingFloatEncodingStrategy { + @inline(__always) + var isThrow: Bool { + if case .throw = self { return true } + return false + } +} + // This is a workaround for the lack of a "set value only if absent" function for Dictionary. extension Optional { mutating func _setIfNil(to value: Wrapped) { From 92562ee027849df0bc15c3512aef6d8388aeae60 Mon Sep 17 00:00:00 2001 From: phoenix Date: Tue, 14 Apr 2026 01:06:40 +0800 Subject: [PATCH 5/7] perf: reuse encoder in wrapGenericEncodable, eliminate array copy + object allocation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- Sources/ReerJSON/JSONEncoderImpl.swift | 86 +++++++++++++------------- 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/Sources/ReerJSON/JSONEncoderImpl.swift b/Sources/ReerJSON/JSONEncoderImpl.swift index e9b0714..626e8e1 100644 --- a/Sources/ReerJSON/JSONEncoderImpl.swift +++ b/Sources/ReerJSON/JSONEncoderImpl.swift @@ -143,6 +143,7 @@ class JSONEncoderImpl: Encoder { // MARK: - Generic Encodable wrapping (T.self == for fast dispatch) func wrapGenericEncodable(_ value: T, for additionalKey: CodingKey?) throws -> UnsafeMutablePointer? { + // Fast-path: special Foundation types (most commonly encountered) if T.self == Date.self { return try wrapDateValue(value as! Date, for: additionalKey) } if T.self == Data.self { return try wrapDataValue(value as! Data, for: additionalKey) } if T.self == URL.self { return wrapString((value as! URL).absoluteString) } @@ -155,6 +156,10 @@ class JSONEncoderImpl: Encoder { } #endif + if T.self is _JSONStringDictionaryEncodableMarker.Type, let dict = value as? [String: Encodable] { + return try wrapStringKeyedDictValue(dict, for: additionalKey) + } + if T.self == [Double].self { return wrapBulkDoubleArray(value as! [Double]) } if T.self == [[Double]].self { return wrapNestedDoubleArray(value as! [[Double]]) } if T.self == [[[Double]]].self { return wrapTripleNestedDoubleArray(value as! [[[Double]]]) } @@ -172,14 +177,7 @@ class JSONEncoderImpl: Encoder { if T.self == [UInt64].self { return wrapBulkUInt64Array(value as! [UInt64]) } if T.self == [Float].self { return try wrapFloatArray(value as! [Float]) } - if T.self is _JSONStringDictionaryEncodableMarker.Type, let dict = value as? [String: Encodable] { - return try wrapStringKeyedDictValue(dict, for: additionalKey) - } - - let newPath = codingPath + (additionalKey.map { [$0] } ?? []) - let sub = JSONEncoderImpl(doc: doc, codingPath: newPath, options: options) - try value.encode(to: sub) - return sub.takeValue() + return try _encodeNestedValue(for: additionalKey) { try value.encode(to: self) } } func wrapEncodable(_ value: Encodable, for additionalKey: CodingKey?) throws -> UnsafeMutablePointer? { @@ -197,15 +195,26 @@ class JSONEncoderImpl: Encoder { } #endif - let newPath = codingPath + (additionalKey.map { [$0] } ?? []) - let sub = JSONEncoderImpl(doc: doc, codingPath: newPath, options: options) - try value.encode(to: sub) - return sub.takeValue() + return try _encodeNestedValue(for: additionalKey) { try value.encode(to: self) } + } + + /// Reuse this encoder for a nested value.encode(to:) call, avoiding array copy + object allocation. + @inline(__always) + private func _encodeNestedValue(for additionalKey: CodingKey?, body: () throws -> Void) rethrows -> UnsafeMutablePointer? { + if let key = additionalKey { codingPath.append(key) } + let savedSV = singleValue; let savedArr = array; let savedObj = object + singleValue = nil; array = nil; object = nil + defer { + singleValue = savedSV; array = savedArr; object = savedObj + if additionalKey != nil { codingPath.removeLast() } + } + try body() + return takeValue() } // MARK: - Bulk array fast paths (single C call, zero Swift loop) - @inline(__always) private func wrapBulkDoubleArray(_ arr: [Double]) -> UnsafeMutablePointer { + @inline(__always) func wrapBulkDoubleArray(_ arr: [Double]) -> UnsafeMutablePointer { if options.nonConformingFloatEncodingStrategy.isThrow { return arr.withUnsafeBufferPointer { yyjson_mut_arr_with_double(doc, $0.baseAddress, $0.count) } } @@ -219,47 +228,47 @@ class JSONEncoderImpl: Encoder { } return result } - @inline(__always) private func wrapBulkBoolArray(_ arr: [Bool]) -> UnsafeMutablePointer { + @inline(__always) func wrapBulkBoolArray(_ arr: [Bool]) -> UnsafeMutablePointer { arr.withUnsafeBufferPointer { yyjson_mut_arr_with_bool(doc, $0.baseAddress, $0.count) } } - @inline(__always) private func wrapBulkInt8Array(_ arr: [Int8]) -> UnsafeMutablePointer { + @inline(__always) func wrapBulkInt8Array(_ arr: [Int8]) -> UnsafeMutablePointer { arr.withUnsafeBufferPointer { yyjson_mut_arr_with_sint8(doc, $0.baseAddress, $0.count) } } - @inline(__always) private func wrapBulkInt16Array(_ arr: [Int16]) -> UnsafeMutablePointer { + @inline(__always) func wrapBulkInt16Array(_ arr: [Int16]) -> UnsafeMutablePointer { arr.withUnsafeBufferPointer { yyjson_mut_arr_with_sint16(doc, $0.baseAddress, $0.count) } } - @inline(__always) private func wrapBulkInt32Array(_ arr: [Int32]) -> UnsafeMutablePointer { + @inline(__always) func wrapBulkInt32Array(_ arr: [Int32]) -> UnsafeMutablePointer { arr.withUnsafeBufferPointer { yyjson_mut_arr_with_sint32(doc, $0.baseAddress, $0.count) } } - @inline(__always) private func wrapBulkInt64Array(_ arr: [Int64]) -> UnsafeMutablePointer { + @inline(__always) func wrapBulkInt64Array(_ arr: [Int64]) -> UnsafeMutablePointer { arr.withUnsafeBufferPointer { yyjson_mut_arr_with_sint64(doc, $0.baseAddress, $0.count) } } - @inline(__always) private func wrapBulkIntArray(_ arr: [Int]) -> UnsafeMutablePointer { + @inline(__always) func wrapBulkIntArray(_ arr: [Int]) -> UnsafeMutablePointer { let mapped = arr.map { Int64($0) } return mapped.withUnsafeBufferPointer { yyjson_mut_arr_with_sint64(doc, $0.baseAddress, $0.count) } } - @inline(__always) private func wrapBulkUInt8Array(_ arr: [UInt8]) -> UnsafeMutablePointer { + @inline(__always) func wrapBulkUInt8Array(_ arr: [UInt8]) -> UnsafeMutablePointer { arr.withUnsafeBufferPointer { yyjson_mut_arr_with_uint8(doc, $0.baseAddress, $0.count) } } - @inline(__always) private func wrapBulkUInt16Array(_ arr: [UInt16]) -> UnsafeMutablePointer { + @inline(__always) func wrapBulkUInt16Array(_ arr: [UInt16]) -> UnsafeMutablePointer { arr.withUnsafeBufferPointer { yyjson_mut_arr_with_uint16(doc, $0.baseAddress, $0.count) } } - @inline(__always) private func wrapBulkUInt32Array(_ arr: [UInt32]) -> UnsafeMutablePointer { + @inline(__always) func wrapBulkUInt32Array(_ arr: [UInt32]) -> UnsafeMutablePointer { arr.withUnsafeBufferPointer { yyjson_mut_arr_with_uint32(doc, $0.baseAddress, $0.count) } } - @inline(__always) private func wrapBulkUInt64Array(_ arr: [UInt64]) -> UnsafeMutablePointer { + @inline(__always) func wrapBulkUInt64Array(_ arr: [UInt64]) -> UnsafeMutablePointer { arr.withUnsafeBufferPointer { yyjson_mut_arr_with_uint64(doc, $0.baseAddress, $0.count) } } - @inline(__always) private func wrapBulkUIntArray(_ arr: [UInt]) -> UnsafeMutablePointer { + @inline(__always) func wrapBulkUIntArray(_ arr: [UInt]) -> UnsafeMutablePointer { let mapped = arr.map { UInt64($0) } return mapped.withUnsafeBufferPointer { yyjson_mut_arr_with_uint64(doc, $0.baseAddress, $0.count) } } - @inline(__always) private func wrapBulkStringArray(_ arr: [String]) -> UnsafeMutablePointer { + @inline(__always) func wrapBulkStringArray(_ arr: [String]) -> UnsafeMutablePointer { let result = yyjson_mut_arr(doc)! for s in arr { yyjson_mut_arr_append(result, wrapString(s)) } return result } - @inline(__always) private func wrapFloatArray(_ arr: [Float]) throws -> UnsafeMutablePointer { + @inline(__always) func wrapFloatArray(_ arr: [Float]) throws -> UnsafeMutablePointer { let result = yyjson_mut_arr(doc)! for f in arr { yyjson_mut_arr_append(result, try wrapFloat(f, for: nil)) } return result @@ -267,13 +276,13 @@ class JSONEncoderImpl: Encoder { // MARK: - Nested double array fast paths (Canada-killer) - private func wrapNestedDoubleArray(_ arr: [[Double]]) -> UnsafeMutablePointer { + func wrapNestedDoubleArray(_ arr: [[Double]]) -> UnsafeMutablePointer { let result = yyjson_mut_arr(doc)! for inner in arr { yyjson_mut_arr_append(result, wrapBulkDoubleArray(inner)) } return result } - private func wrapTripleNestedDoubleArray(_ arr: [[[Double]]]) -> UnsafeMutablePointer { + func wrapTripleNestedDoubleArray(_ arr: [[[Double]]]) -> UnsafeMutablePointer { let result = yyjson_mut_arr(doc)! for inner in arr { yyjson_mut_arr_append(result, wrapNestedDoubleArray(inner)) } return result @@ -284,9 +293,7 @@ class JSONEncoderImpl: Encoder { func wrapDateValue(_ date: Date, for additionalKey: CodingKey?) throws -> UnsafeMutablePointer? { switch options.dateEncodingStrategy { case .deferredToDate: - let sub = JSONEncoderImpl(doc: doc, codingPath: codingPath + (additionalKey.map { [$0] } ?? []), options: options) - try date.encode(to: sub) - return sub.takeValue() + return try _encodeNestedValue(for: additionalKey) { try date.encode(to: self) } case .secondsSince1970: return try wrapFloat(date.timeIntervalSince1970, for: additionalKey) case .millisecondsSince1970: @@ -296,9 +303,7 @@ class JSONEncoderImpl: Encoder { case .formatted(let formatter): return wrapString(formatter.string(from: date)) case .custom(let closure): - let sub = JSONEncoderImpl(doc: doc, codingPath: codingPath + (additionalKey.map { [$0] } ?? []), options: options) - try closure(date, sub) - return sub.takeValue() ?? yyjson_mut_obj(doc) + return try _encodeNestedValue(for: additionalKey) { try closure(date, self) } ?? yyjson_mut_obj(doc) @unknown default: fatalError() } } @@ -306,15 +311,11 @@ class JSONEncoderImpl: Encoder { func wrapDataValue(_ data: Data, for additionalKey: CodingKey?) throws -> UnsafeMutablePointer? { switch options.dataEncodingStrategy { case .deferredToData: - let sub = JSONEncoderImpl(doc: doc, codingPath: codingPath + (additionalKey.map { [$0] } ?? []), options: options) - try data.encode(to: sub) - return sub.takeValue() + return try _encodeNestedValue(for: additionalKey) { try data.encode(to: self) } case .base64: return wrapString(data.base64EncodedString()) case .custom(let closure): - let sub = JSONEncoderImpl(doc: doc, codingPath: codingPath + (additionalKey.map { [$0] } ?? []), options: options) - try closure(data, sub) - return sub.takeValue() ?? yyjson_mut_obj(doc) + return try _encodeNestedValue(for: additionalKey) { try closure(data, self) } ?? yyjson_mut_obj(doc) @unknown default: fatalError() } } @@ -325,11 +326,8 @@ class JSONEncoderImpl: Encoder { if let additionalKey { codingPath.append(additionalKey) } for (key, value) in dict { let keyVal = wrapString(key) - codingPath.append(_CodingKey(stringValue: key)!) - let sub = JSONEncoderImpl(doc: doc, codingPath: codingPath, options: options) - let val = try sub.wrapEncodable(value, for: nil) ?? yyjson_mut_obj(doc)! + let val = try wrapEncodable(value, for: _CodingKey(stringValue: key)!) ?? yyjson_mut_obj(doc)! yyjson_mut_obj_add(obj, keyVal, val) - codingPath.removeLast() } codingPath = savedPath return obj From c33de89d596297b50e21ff62ff23a3b243f1c81e Mon Sep 17 00:00:00 2001 From: phoenix Date: Tue, 14 Apr 2026 10:21:45 +0800 Subject: [PATCH 6/7] perf: optimize YYJSONKeyedEncodingContainer encoder performance - 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. --- Sources/ReerJSON/JSONEncoderImpl.swift | 71 +++++++++++++++++--------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/Sources/ReerJSON/JSONEncoderImpl.swift b/Sources/ReerJSON/JSONEncoderImpl.swift index 626e8e1..4a98fe9 100644 --- a/Sources/ReerJSON/JSONEncoderImpl.swift +++ b/Sources/ReerJSON/JSONEncoderImpl.swift @@ -134,9 +134,7 @@ class JSONEncoderImpl: Encoder { func wrapString(_ string: String) -> UnsafeMutablePointer { var s = string return s.withUTF8 { buf in - buf.baseAddress!.withMemoryRebound(to: CChar.self, capacity: buf.count) { ptr in - yyjson_mut_strncpy(doc, ptr, buf.count) - } + yyjson_mut_strncpy(doc, buf.baseAddress, buf.count) } } @@ -407,37 +405,60 @@ private struct YYJSONKeyedEncodingContainer: KeyedEncodingContaine let impl: JSONEncoderImpl var codingPath: [CodingKey] let object: UnsafeMutablePointer - var doc: UnsafeMutablePointer { impl.doc } + let doc: UnsafeMutablePointer + let useDefaultKeys: Bool + + init(impl: JSONEncoderImpl, codingPath: [CodingKey], object: UnsafeMutablePointer) { + self.impl = impl + self.codingPath = codingPath + self.object = object + self.doc = impl.doc + if case .useDefaultKeys = impl.options.keyEncodingStrategy { + self.useDefaultKeys = true + } else { + self.useDefaultKeys = false + } + } + + @inline(__always) + private func _key(_ key: Key) -> String { + useDefaultKeys ? key.stringValue : impl.convertedKey(key) + } + + @inline(__always) + private func _strVal(_ s: String) -> UnsafeMutablePointer { + impl.wrapString(s) + } @inline(__always) private func addToObject(key: String, value: UnsafeMutablePointer) { - yyjson_mut_obj_put(object, impl.wrapString(key), value) - } - - mutating func encodeNil(forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: yyjson_mut_null(doc)) } - mutating func encode(_ value: Bool, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: yyjson_mut_bool(doc, value)) } - mutating func encode(_ value: String, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapString(value)) } - mutating func encode(_ value: Double, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: try impl.wrapFloat(value, for: key)) } - mutating func encode(_ value: Float, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: try impl.wrapFloat(value, for: key)) } - mutating func encode(_ value: Int, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapInt(value)) } - mutating func encode(_ value: Int8, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapInt(value)) } - mutating func encode(_ value: Int16, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapInt(value)) } - mutating func encode(_ value: Int32, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapInt(value)) } - mutating func encode(_ value: Int64, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapInt(value)) } - mutating func encode(_ value: UInt, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapUInt(value)) } - mutating func encode(_ value: UInt8, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapUInt(value)) } - mutating func encode(_ value: UInt16, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapUInt(value)) } - mutating func encode(_ value: UInt32, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapUInt(value)) } - mutating func encode(_ value: UInt64, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapUInt(value)) } + yyjson_mut_obj_put(object, _strVal(key), value) + } + + mutating func encodeNil(forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_null(doc)) } + mutating func encode(_ value: Bool, forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_bool(doc, value)) } + mutating func encode(_ value: String, forKey key: Key) throws { addToObject(key: _key(key), value: _strVal(value)) } + mutating func encode(_ value: Double, forKey key: Key) throws { addToObject(key: _key(key), value: try impl.wrapFloat(value, for: key)) } + mutating func encode(_ value: Float, forKey key: Key) throws { addToObject(key: _key(key), value: try impl.wrapFloat(value, for: key)) } + mutating func encode(_ value: Int, forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_sint(doc, Int64(value))) } + mutating func encode(_ value: Int8, forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_sint(doc, Int64(value))) } + mutating func encode(_ value: Int16, forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_sint(doc, Int64(value))) } + mutating func encode(_ value: Int32, forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_sint(doc, Int64(value))) } + mutating func encode(_ value: Int64, forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_sint(doc, Int64(value))) } + mutating func encode(_ value: UInt, forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_uint(doc, UInt64(value))) } + mutating func encode(_ value: UInt8, forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_uint(doc, UInt64(value))) } + mutating func encode(_ value: UInt16, forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_uint(doc, UInt64(value))) } + mutating func encode(_ value: UInt32, forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_uint(doc, UInt64(value))) } + mutating func encode(_ value: UInt64, forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_uint(doc, UInt64(value))) } #if compiler(>=6.0) @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) - mutating func encode(_ value: Int128, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapInt128(value)) } + mutating func encode(_ value: Int128, forKey key: Key) throws { addToObject(key: _key(key), value: impl.wrapInt128(value)) } @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) - mutating func encode(_ value: UInt128, forKey key: Key) throws { addToObject(key: impl.convertedKey(key), value: impl.wrapUInt128(value)) } + mutating func encode(_ value: UInt128, forKey key: Key) throws { addToObject(key: _key(key), value: impl.wrapUInt128(value)) } #endif mutating func encode(_ value: T, forKey key: Key) throws { - addToObject(key: impl.convertedKey(key), value: try impl.wrapGenericEncodable(value, for: key) ?? yyjson_mut_obj(doc)!) + addToObject(key: _key(key), value: try impl.wrapGenericEncodable(value, for: key) ?? yyjson_mut_obj(doc)!) } mutating func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer { From 285dc33248c128bc68d3a09aef7ecb5c77528485 Mon Sep 17 00:00:00 2001 From: phoenix Date: Tue, 14 Apr 2026 10:35:45 +0800 Subject: [PATCH 7/7] refactor: remove nested double array fast paths 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 --- Sources/ReerJSON/JSONEncoderImpl.swift | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/Sources/ReerJSON/JSONEncoderImpl.swift b/Sources/ReerJSON/JSONEncoderImpl.swift index 4a98fe9..87a3368 100644 --- a/Sources/ReerJSON/JSONEncoderImpl.swift +++ b/Sources/ReerJSON/JSONEncoderImpl.swift @@ -159,8 +159,7 @@ class JSONEncoderImpl: Encoder { } if T.self == [Double].self { return wrapBulkDoubleArray(value as! [Double]) } - if T.self == [[Double]].self { return wrapNestedDoubleArray(value as! [[Double]]) } - if T.self == [[[Double]]].self { return wrapTripleNestedDoubleArray(value as! [[[Double]]]) } + if T.self == [String].self { return wrapBulkStringArray(value as! [String]) } if T.self == [Bool].self { return wrapBulkBoolArray(value as! [Bool]) } if T.self == [Int].self { return wrapBulkIntArray(value as! [Int]) } @@ -272,20 +271,6 @@ class JSONEncoderImpl: Encoder { return result } - // MARK: - Nested double array fast paths (Canada-killer) - - func wrapNestedDoubleArray(_ arr: [[Double]]) -> UnsafeMutablePointer { - let result = yyjson_mut_arr(doc)! - for inner in arr { yyjson_mut_arr_append(result, wrapBulkDoubleArray(inner)) } - return result - } - - func wrapTripleNestedDoubleArray(_ arr: [[[Double]]]) -> UnsafeMutablePointer { - let result = yyjson_mut_arr(doc)! - for inner in arr { yyjson_mut_arr_append(result, wrapNestedDoubleArray(inner)) } - return result - } - // MARK: - Date / Data / Dict wrapping func wrapDateValue(_ date: Date, for additionalKey: CodingKey?) throws -> UnsafeMutablePointer? {