Skip to content

Instantly share code, notes, and snippets.

@couchdeveloper
Last active January 17, 2026 21:39
Show Gist options
  • Select an option

  • Save couchdeveloper/8a95dea4db78d53a8d3abc8968641dcb to your computer and use it in GitHub Desktop.

Select an option

Save couchdeveloper/8a95dea4db78d53a8d3abc8968641dcb to your computer and use it in GitHub Desktop.
Static JSON Encoder
import struct Foundation.Date
import class Foundation.DateFormatter
import class Foundation.ISO8601DateFormatter
// MARK: - Root Tag Protocol
/// Root protocol for all encoding strategies.
///
/// This is a tag protocol that enables generic constraints and extensions.
public protocol EncodingStrategies: Sendable {}
// MARK: - Date Encoding Precision
/// The precision of date encoding (for timestamps).
///
/// - `seconds`: Encodes with second precision
/// - `milliseconds`: Encodes with millisecond precision
/// - `formatted`: Uses a formatter (custom precision)
public enum DateEncodingStrategyPrecision: Sendable {
case seconds
case milliseconds
case formatted
}
// MARK: - Individual Strategy Protocols (Lego Bricks)
/// Protocol for date encoding strategies.
///
/// Conforming types provide a static method for encoding dates to strings,
/// along with metadata and capabilities that enable compile-time optimization
/// and specialization.
public protocol DateEncodingStrategy: Sendable {
/// The precision of the date encoding (for timestamps).
typealias Precision = DateEncodingStrategyPrecision
/// Indicates whether this strategy uses a formatter (vs raw numeric conversion).
///
/// - `true`: Uses DateFormatter or ISO8601DateFormatter
/// - `false`: Direct numeric conversion (timestamps)
static var usesFormatter: Bool { get }
/// The precision of the encoded date.
static var precision: Precision { get }
/// A human-readable description of the strategy.
static var description: String { get }
/// Indicates whether the formatter is cached (if applicable).
///
/// Only relevant when `usesFormatter == true`.
static var cacheFormatter: Bool { get }
/// Encodes a date to a string representation.
///
/// - Parameter date: The date to encode
/// - Returns: The string representation of the date
/// - Throws: An error if encoding fails
static func encode(_ date: Date) throws -> String
}
/// Protocol for array key encoding strategies.
///
/// Determines how array elements are represented in parameter names.
public protocol ArrayKeyEncodingStrategy: Sendable {
/// Encodes an array element key.
///
/// - Parameters:
/// - base: The base key (parent) or empty string for top-level
/// - isEmpty: True if using bracket-style notation (empty string key)
/// - index: The array index (for indices style), or nil
/// - Returns: The formatted key for the array element
static func encodeKey(base: String, isEmpty: Bool, index: Int?) -> String
}
/// Protocol for object (nested) key encoding strategies.
///
/// Determines how nested object properties are represented.
public protocol ObjectKeyEncodingStrategy: Sendable {
/// Encodes a nested object key.
///
/// - Parameters:
/// - base: The base key (parent)
/// - segment: The nested property name
/// - Returns: The formatted key combining base and segment
static func encodeKey(base: String, segment: String) -> String
}
/// Protocol for key transformation strategies.
///
/// Transforms coding key names (e.g., camelCase to snake_case).
public protocol KeyTransformStrategy: Sendable {
/// Transforms a coding key according to the naming strategy.
///
/// - Parameter key: The original key string
/// - Returns: The transformed key string
static func transform(_ key: String) -> String
}
/// Protocol for top-level key strategies.
///
/// Handles how top-level values without explicit keys are named.
public protocol TopLevelKeyStrategy: Sendable {
/// Handles the top-level key.
///
/// - Parameter suggestedKey: A suggested key (may be empty)
/// - Returns: The actual key to use, or empty string to allow empty keys
static func topLevelKey(_ suggestedKey: String) -> String
}
// MARK: - Concrete Date Strategies
/// ISO8601 date encoding strategy with cached formatter.
///
/// The formatter is created once and reused for all encoding operations,
/// avoiding the expensive cost of creating a new formatter each time.
public struct ISO8601DateFormatterStrategy: DateEncodingStrategy {
@available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *)
@usableFromInline
nonisolated(unsafe) static let formatter = ISO8601DateFormatter()
public static let usesFormatter: Bool = true
public static let precision: Precision = .formatted
public static let description: String = "ISO8601 formatted dates"
public static let cacheFormatter: Bool = true
@inlinable
public static func encode(_ date: Date) throws -> String {
if #available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *) {
return formatter.string(from: date)
} else {
return String(date.timeIntervalSince1970)
}
}
}
/// Unix timestamp date encoding strategy.
///
/// Encodes dates as seconds since January 1, 1970 (Unix epoch).
/// No formatter needed - zero allocation overhead.
public struct UnixTimestampStrategy: DateEncodingStrategy {
public static let usesFormatter: Bool = false
public static let precision: Precision = .seconds
public static let description: String = "Unix timestamp (seconds since 1970)"
public static let cacheFormatter: Bool = false
@inlinable
public static func encode(_ date: Date) throws -> String {
String(date.timeIntervalSince1970)
}
}
/// Reference date timestamp encoding strategy.
///
/// Encodes dates as seconds since the reference date (January 1, 2001).
/// No formatter needed - zero allocation overhead.
public struct ReferenceDateTimestampStrategy: DateEncodingStrategy {
public static let usesFormatter: Bool = false
public static let precision: Precision = .seconds
public static let description: String = "Reference date timestamp (seconds since 2001)"
public static let cacheFormatter: Bool = false
@inlinable
public static func encode(_ date: Date) throws -> String {
String(date.timeIntervalSinceReferenceDate)
}
}
// MARK: - Concrete Array Key Strategies
/// Brackets array key encoding strategy.
///
/// Produces keys like: `items[]=1&items[]=2`
public struct BracketsArrayKeyStrategy: ArrayKeyEncodingStrategy {
@inlinable
public static func encodeKey(base: String, isEmpty: Bool, index: Int?) -> String {
if isEmpty {
return base.isEmpty ? "" : base + "[]"
} else if let index = index {
return base.isEmpty ? String(index) : "\(base)[\(index)]"
}
return base
}
}
/// Indices array key encoding strategy.
///
/// Produces keys like: `items[0]=1&items[1]=2`
public struct IndicesArrayKeyStrategy: ArrayKeyEncodingStrategy {
@inlinable
public static func encodeKey(base: String, isEmpty: Bool, index: Int?) -> String {
if let index = index {
return base.isEmpty ? String(index) : "\(base)[\(index)]"
}
return base
}
}
/// No brackets array key encoding strategy.
///
/// Produces keys like: `items=1&items=2`
public struct NoBracketsArrayKeyStrategy: ArrayKeyEncodingStrategy {
@inlinable
public static func encodeKey(base: String, isEmpty: Bool, index: Int?) -> String {
base
}
}
// MARK: - Concrete Object Key Strategies
/// Brackets object key encoding strategy.
///
/// Produces keys like: `user[name]=Ana&user[address][city]=NYC`
public struct BracketsObjectKeyStrategy: ObjectKeyEncodingStrategy {
@inlinable
public static func encodeKey(base: String, segment: String) -> String {
if base.isEmpty {
return segment
}
return "\(base)[\(segment)]"
}
}
/// Dot notation object key encoding strategy.
///
/// Produces keys like: `user.name=Ana&user.address.city=NYC`
public struct DotNotationObjectKeyStrategy: ObjectKeyEncodingStrategy {
@inlinable
public static func encodeKey(base: String, segment: String) -> String {
if base.isEmpty {
return segment
}
return "\(base).\(segment)"
}
}
// MARK: - Concrete Key Transform Strategies
/// Identity key transformation (no transformation).
public struct IdentityKeyTransform: KeyTransformStrategy {
@inlinable
public static func transform(_ key: String) -> String {
key
}
}
/// Snake case key transformation.
///
/// Converts `camelCase` to `snake_case`.
public struct SnakeCaseKeyTransform: KeyTransformStrategy {
@inlinable
public static func transform(_ key: String) -> String {
guard !key.isEmpty else { return key }
var result = ""
var previousWasUppercase = false
var index = key.startIndex
while index < key.endIndex {
let char = key[index]
if char.isUppercase {
if index != key.startIndex && !previousWasUppercase {
result.append("_")
}
result.append(char.lowercased())
previousWasUppercase = true
} else {
result.append(char)
previousWasUppercase = false
}
index = key.index(after: index)
}
return result
}
}
// MARK: - Concrete Top-Level Key Strategies
/// Allow empty top-level keys.
///
/// Top-level primitives/arrays can have empty keys: `=value` or `[]=item`
public struct AllowEmptyTopLevelKeyStrategy: TopLevelKeyStrategy {
@inlinable
public static func topLevelKey(_ suggestedKey: String) -> String {
suggestedKey
}
}
/// Require a specific root key for top-level values.
public struct RequireRootKeyStrategy: TopLevelKeyStrategy {
public static let rootKey: String = "root"
@inlinable
public static func topLevelKey(_ suggestedKey: String) -> String {
suggestedKey.isEmpty ? rootKey : suggestedKey
}
}
// MARK: - Concrete Composite Strategy Types
/// Standard query string encoding strategies with brackets notation.
///
/// - Arrays: `items[]=1&items[]=2`
/// - Objects: `user[name]=Ana`
/// - Dates: ISO8601
/// - Keys: As-is
public struct BracketsEncodingStrategies: URLEncodingStrategies {
public typealias DateStrategy = ISO8601DateFormatterStrategy
public typealias ArrayKeyStrategy = BracketsArrayKeyStrategy
public typealias ObjectKeyStrategy = BracketsObjectKeyStrategy
public typealias KeyTransform = IdentityKeyTransform
public typealias TopLevelKey = AllowEmptyTopLevelKeyStrategy
public init() {}
}
/// URL template encoding strategies with dot notation.
///
/// - Arrays: `items[]=1&items[]=2`
/// - Objects: `user.name=Ana` (dot notation)
/// - Dates: ISO8601
/// - Keys: As-is
public struct DotNotationEncodingStrategies: URLEncodingStrategies {
public typealias DateStrategy = ISO8601DateFormatterStrategy
public typealias ArrayKeyStrategy = BracketsArrayKeyStrategy
public typealias ObjectKeyStrategy = DotNotationObjectKeyStrategy
public typealias KeyTransform = IdentityKeyTransform
public typealias TopLevelKey = AllowEmptyTopLevelKeyStrategy
public init() {}
}
/// Array encoding with indices instead of brackets.
///
/// - Arrays: `items[0]=1&items[1]=2`
/// - Objects: `user[name]=Ana`
/// - Dates: ISO8601
/// - Keys: As-is
public struct IndicesEncodingStrategies: URLEncodingStrategies {
public typealias DateStrategy = ISO8601DateFormatterStrategy
public typealias ArrayKeyStrategy = IndicesArrayKeyStrategy
public typealias ObjectKeyStrategy = BracketsObjectKeyStrategy
public typealias KeyTransform = IdentityKeyTransform
public typealias TopLevelKey = AllowEmptyTopLevelKeyStrategy
public init() {}
}
/// Snake case key transformation strategy.
///
/// - Arrays: `items[]=1&items[]=2`
/// - Objects: `user[home_address]=value`
/// - Dates: ISO8601
/// - Keys: Converted to snake_case
public struct SnakeCaseEncodingStrategies: URLEncodingStrategies {
public typealias DateStrategy = ISO8601DateFormatterStrategy
public typealias ArrayKeyStrategy = BracketsArrayKeyStrategy
public typealias ObjectKeyStrategy = BracketsObjectKeyStrategy
public typealias KeyTransform = SnakeCaseKeyTransform
public typealias TopLevelKey = AllowEmptyTopLevelKeyStrategy
public init() {}
}
/// Unix timestamp dates strategy.
///
/// - Arrays: `items[]=1&items[]=2`
/// - Objects: `user[name]=Ana`
/// - Dates: Unix timestamp
/// - Keys: As-is
public struct UnixTimestampEncodingStrategies: URLEncodingStrategies {
public typealias DateStrategy = UnixTimestampStrategy
public typealias ArrayKeyStrategy = BracketsArrayKeyStrategy
public typealias ObjectKeyStrategy = BracketsObjectKeyStrategy
public typealias KeyTransform = IdentityKeyTransform
public typealias TopLevelKey = AllowEmptyTopLevelKeyStrategy
public init() {}
}
/// Combined dot notation + snake_case strategy.
///
/// - Arrays: `items[]=1&items[]=2`
/// - Objects: `user.home_address=value`
/// - Dates: ISO8601
/// - Keys: snake_case
public struct DotNotationSnakeCaseStrategies: URLEncodingStrategies {
public typealias DateStrategy = ISO8601DateFormatterStrategy
public typealias ArrayKeyStrategy = BracketsArrayKeyStrategy
public typealias ObjectKeyStrategy = DotNotationObjectKeyStrategy
public typealias KeyTransform = SnakeCaseKeyTransform
public typealias TopLevelKey = AllowEmptyTopLevelKeyStrategy
public init() {}
}
import struct Foundation.Data
import struct Foundation.Date
// MARK: - JSON-Specific Strategy Protocols
/// Protocol for data encoding strategies in JSON.
///
/// Determines how `Data` values are encoded (base64, hex, etc.).
public protocol DataEncodingStrategy: Sendable {
/// Encodes a Data value to a string representation.
static func encode(_ data: Data) throws -> String
}
/// Protocol for floating point encoding strategies in JSON.
///
/// Determines how special floating point values (NaN, Infinity) are handled.
public protocol FloatingPointEncodingStrategy: Sendable {
/// Whether to throw an error on non-conforming floats (NaN, Infinity).
static var throwOnNonConformingFloat: Bool { get }
/// Encodes a non-conforming float (only called if throwOnNonConformingFloat is false).
static func encodeNonConformingFloat(_ value: Double) -> String
}
// MARK: - JSON Encoding Strategies Protocol
/// Composite protocol for JSON encoding strategies.
///
/// Combines the necessary strategy components for JSON encoding.
public protocol JSONEncodingStrategies: EncodingStrategies {
/// The date encoding strategy to use.
associatedtype DateStrategy: DateEncodingStrategy
/// The key transformation strategy to use.
associatedtype KeyTransform: KeyTransformStrategy
/// The data encoding strategy to use.
associatedtype DataStrategy: DataEncodingStrategy
/// The floating point encoding strategy to use.
associatedtype FloatingPointStrategy: FloatingPointEncodingStrategy
}
// MARK: - Concrete Data Strategies
/// Base64 data encoding strategy (JSON standard).
public struct Base64DataStrategy: DataEncodingStrategy {
@inlinable
public static func encode(_ data: Data) throws -> String {
data.base64EncodedString()
}
}
/// Hex string data encoding strategy.
public struct HexDataStrategy: DataEncodingStrategy {
@inlinable
public static func encode(_ data: Data) throws -> String {
data.map { String(format: "%02x", $0) }.joined()
}
}
// MARK: - Concrete Floating Point Strategies
/// Strict floating point strategy (throws on NaN/Infinity).
public struct StrictFloatingPointStrategy: FloatingPointEncodingStrategy {
public static let throwOnNonConformingFloat: Bool = true
@inlinable
public static func encodeNonConformingFloat(_ value: Double) -> String {
fatalError("Non-conforming float encountered")
}
}
/// Permissive floating point strategy (encodes NaN/Infinity as strings).
public struct PermissiveFloatingPointStrategy: FloatingPointEncodingStrategy {
public static let throwOnNonConformingFloat: Bool = false
@inlinable
public static func encodeNonConformingFloat(_ value: Double) -> String {
if value.isNaN {
return "\"NaN\""
} else if value == .infinity {
return "\"Infinity\""
} else if value == -.infinity {
return "\"-Infinity\""
}
return "\(value)"
}
}
// MARK: - Concrete JSON Strategy Combinations
/// Standard JSON encoding strategies.
///
/// - Dates: ISO8601
/// - Keys: As-is
/// - Data: Base64
/// - Floats: Strict (throw on NaN/Infinity)
public struct StandardJSONEncodingStrategies: JSONEncodingStrategies {
public typealias DateStrategy = ISO8601DateFormatterStrategy
public typealias KeyTransform = IdentityKeyTransform
public typealias DataStrategy = Base64DataStrategy
public typealias FloatingPointStrategy = StrictFloatingPointStrategy
}
/// Snake case JSON encoding strategies.
public struct SnakeCaseJSONEncodingStrategies: JSONEncodingStrategies {
public typealias DateStrategy = ISO8601DateFormatterStrategy
public typealias KeyTransform = SnakeCaseKeyTransform
public typealias DataStrategy = Base64DataStrategy
public typealias FloatingPointStrategy = StrictFloatingPointStrategy
}
/// Unix timestamp JSON encoding strategies.
public struct UnixTimestampJSONEncodingStrategies: JSONEncodingStrategies {
public typealias DateStrategy = UnixTimestampStrategy
public typealias KeyTransform = IdentityKeyTransform
public typealias DataStrategy = Base64DataStrategy
public typealias FloatingPointStrategy = StrictFloatingPointStrategy
}
import struct Foundation.Data
/// Protocol for JSON output sinks.
///
/// A JSON sink receives hierarchical JSON encoding events and produces output.
public protocol JSONSink: DefaultConstructible, Sendable {
associatedtype Output: Sendable
/// Writes a null value.
mutating func writeNull()
/// Writes a boolean value.
mutating func writeBool(_ value: Bool)
/// Writes a string value (will be escaped).
mutating func writeString(_ value: String)
/// Writes a number value as a string.
mutating func writeNumber(_ value: String)
/// Begins a JSON object.
mutating func beginObject()
/// Ends a JSON object.
mutating func endObject()
/// Begins a JSON array.
mutating func beginArray()
/// Ends a JSON array.
mutating func endArray()
/// Writes a key in an object (will be escaped).
mutating func writeKey(_ key: String)
/// Returns the final output.
func output() -> Output
}
// MARK: - High-Performance Data-based JSON Sink
/// A high-performance JSON sink that builds JSON as Data.
///
/// **Performance optimizations:**
/// - Pre-allocated buffer with capacity estimation
/// - Direct UTF-8 byte writing with unsafe buffers
/// - Zero-copy string operations where possible
/// - Minimal allocations
public struct JSONDataSink: JSONSink {
@usableFromInline
static let nullBytes: [UInt8] = [110, 117, 108, 108] // "null"
@usableFromInline
static let trueBytes: [UInt8] = [116, 114, 117, 101] // "true"
@usableFromInline
static let falseBytes: [UInt8] = [102, 97, 108, 115, 101] // "false"
@usableFromInline
static let unicodeEscapePrefix: [UInt8] = [92, 117, 48, 48] // "\u00"
// Common escape sequences as static constants
@usableFromInline
static let escapeQuote: [UInt8] = [92, 34] // \"
@usableFromInline
static let escapeBackslash: [UInt8] = [92, 92] // \\
@usableFromInline
static let escapeNewline: [UInt8] = [92, 110] // \n
@usableFromInline
static let escapeReturn: [UInt8] = [92, 114] // \r
@usableFromInline
static let escapeTab: [UInt8] = [92, 116] // \t
@usableFromInline
var buffer: [UInt8]
public init() {
// Pre-allocate reasonable capacity to reduce reallocations
self.buffer = []
self.buffer.reserveCapacity(4096)
}
@inlinable
mutating func writeComma() {
buffer.append(44) // ASCII comma
}
@inlinable
mutating func removeTrailingCommaIfNeeded() {
// Remove trailing comma from empty or last element
if buffer.last == 44 {
buffer.removeLast()
}
}
@inlinable
public mutating func writeNull() {
buffer.append(contentsOf: Self.nullBytes)
writeComma()
}
@inlinable
public mutating func writeBool(_ value: Bool) {
buffer.append(contentsOf: value ? Self.trueBytes : Self.falseBytes)
writeComma()
}
@inlinable
public mutating func writeString(_ value: String) {
buffer.append(34) // Opening quote
writeEscapedStringFast(value)
buffer.append(34) // Closing quote
writeComma()
}
@inlinable
public mutating func writeNumber(_ value: String) {
// Fast path: write UTF-8 bytes directly
value.utf8.withContiguousStorageIfAvailable { utf8 in
buffer.append(contentsOf: utf8)
} ?? value.utf8.forEach { buffer.append($0) }
writeComma()
}
@inlinable
public mutating func beginObject() {
buffer.append(123) // {
}
@inlinable
public mutating func endObject() {
removeTrailingCommaIfNeeded()
buffer.append(125) // }
writeComma()
}
@inlinable
public mutating func beginArray() {
buffer.append(91) // [
}
@inlinable
public mutating func endArray() {
removeTrailingCommaIfNeeded()
buffer.append(93) // ]
writeComma()
}
@inlinable
public mutating func writeKey(_ key: String) {
buffer.append(34) // Opening quote
writeEscapedStringFast(key)
buffer.append(34) // Closing quote
buffer.append(58) // :
}
@inlinable
public func output() -> Data {
var finalBuffer = buffer
// Remove trailing comma from root value
if finalBuffer.last == 44 {
finalBuffer.removeLast()
}
return Data(finalBuffer)
}
// MARK: - Fast String Escaping
@usableFromInline
mutating func writeEscapedStringFast(_ string: String) {
// Use UTF8 view for zero-copy access
let utf8Bytes = string.utf8
// Fast scan: check if escaping is needed
var needsEscaping = false
for byte in utf8Bytes {
if byte == 34 || byte == 92 || byte < 32 {
needsEscaping = true
break
}
}
if !needsEscaping {
// Fast path: no escaping needed, bulk copy if contiguous
utf8Bytes.withContiguousStorageIfAvailable { bytes in
buffer.append(contentsOf: bytes)
} ?? utf8Bytes.forEach { buffer.append($0) }
} else {
// Slow path: escape as needed
let oldCount = buffer.count
let estimatedSize = utf8Bytes.count + (utf8Bytes.count >> 3) // Estimate 12.5% escaping overhead
buffer.reserveCapacity(oldCount + estimatedSize)
for byte in utf8Bytes {
switch byte {
case 34: // "
buffer.append(contentsOf: Self.escapeQuote)
case 92: // \
buffer.append(contentsOf: Self.escapeBackslash)
case 10: // \n
buffer.append(contentsOf: Self.escapeNewline)
case 13: // \r
buffer.append(contentsOf: Self.escapeReturn)
case 9: // \t
buffer.append(contentsOf: Self.escapeTab)
case 0..<32: // Other control characters
// \u00XX
writeControlCharacterEscape(byte)
default:
buffer.append(byte)
}
}
}
}
@usableFromInline
mutating func writeControlCharacterEscape(_ byte: UInt8) {
// \u00XX format - write directly as bytes
buffer.append(contentsOf: Self.unicodeEscapePrefix)
// Hex digits
let high = byte >> 4
let low = byte & 0x0F
buffer.append(high < 10 ? (48 + high) : (87 + high)) // 0-9 or a-f
buffer.append(low < 10 ? (48 + low) : (87 + low))
}
}
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2026] [Andreas Grosam]
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
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
import struct Foundation.Date
import struct Foundation.Data
import struct Foundation.URL
/// A compile-time optimized JSON encoder using static strategy dispatch.
///
/// This encoder is a pure function with no state. Its configuration is entirely
/// in the type parameters (Sink and Strategies), enabling aggressive compiler
/// optimizations through static dispatch.
///
/// **Conceptual Model:**
/// The encoder is fundamentally a pure function: `(Encodable) throws -> Output`
/// - The TYPE is the configuration
/// - No stored state
/// - Given same input, always produces same output
///
/// **Performance Benefits:**
/// - Zero runtime overhead (no enum switches)
/// - Direct static method dispatch
/// - Aggressive inlining by compiler
/// - Dead code elimination
/// - No stored state (zero-size type)
///
/// **Usage:**
/// ```swift
/// // The encoder is a pure function
/// let encoder = StaticJSONEncoder<JSONStringSink, StandardJSONEncodingStrategies>()
/// let json = try encoder.encode(myValue)
///
/// // Or use static method directly
/// let json = try StaticJSONEncoder<JSONStringSink, StandardJSONEncodingStrategies>.encode(myValue)
/// ```
public struct StaticJSONEncoder<Sink: JSONSink, Strategies: JSONEncodingStrategies>: Sendable {
public init() {}
/// Encodes the given value and returns the output directly.
///
/// This is a pure function - given the same input, it always produces the same output.
/// The encoder has no state; its configuration is entirely in the type parameters.
///
/// - Parameter value: The value to encode
/// - Returns: The encoded output
/// - Throws: An error if encoding fails
@inlinable
public func encode<T: Encodable>(_ value: T) throws -> Sink.Output {
try Self.encode(value)
}
/// Static encoding function.
///
/// Since the encoder is a pure function with no state, this static method
/// provides the core functionality. The instance method delegates to this.
///
/// - Parameter value: The value to encode
/// - Returns: The encoded output
/// - Throws: An error if encoding fails
@inlinable
public static func encode<T: Encodable>(_ value: T) throws -> Sink.Output {
let state = _StaticJSONEncoderState(sink: Sink())
let encoder = _StaticJSONEncoder<Sink, Strategies>(state: state)
try value.encode(to: encoder)
return state.sink.output()
}
/// Creates a type-erased encoder that hides the Sink and Strategies types.
///
/// - Returns: A type-erased encoder that only exposes the output type
@inlinable
public func eraseToAnyEncoder() -> AnyJSONEncoder<Sink.Output> {
AnyJSONEncoder(self)
}
}
// MARK: - Type-Erased Encoder
/// A type-erased JSON encoder.
///
/// This type hides the concrete Sink and Strategies types, exposing only
/// the output type.
public struct AnyJSONEncoder<Output>: Sendable where Output: Sendable {
public let _encode: @Sendable (any Encodable) throws -> Output
/// Creates a type-erased encoder from a concrete encoder.
@inlinable
public init<Sink, Strategies>(
_ encoder: StaticJSONEncoder<Sink, Strategies>
) where Sink.Output == Output {
self._encode = { value in
try encoder.encode(value)
}
}
/// Encodes a value and returns the output.
@inlinable
public func encode<T: Encodable>(_ value: T) throws -> Output {
try _encode(value)
}
}
// MARK: - Internal Encoder Implementation
/// Mutable state for the encoder.
/// This is a class because it needs to be shared across all containers.
@usableFromInline
internal final class _StaticJSONEncoderState<Sink: JSONSink> {
@usableFromInline var codingPath: [CodingKey] = []
@usableFromInline var userInfo: [CodingUserInfoKey: Any] = [:]
@usableFromInline var sink: Sink
@usableFromInline
init(sink: Sink) {
self.sink = sink
}
}
/// The encoder struct conforming to the Encoder protocol.
/// This is a struct with no stored state - it just holds a reference to the State class.
@usableFromInline
internal struct _StaticJSONEncoder<Sink: JSONSink, Strategies: JSONEncodingStrategies>: Encoder {
@usableFromInline let state: _StaticJSONEncoderState<Sink>
@usableFromInline var codingPath: [CodingKey] { state.codingPath }
@usableFromInline var userInfo: [CodingUserInfoKey: Any] { state.userInfo }
@usableFromInline
init(state: _StaticJSONEncoderState<Sink>) {
self.state = state
}
@inlinable
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key: CodingKey {
let container = _StaticJSONKeyedContainer<Key, Sink, Strategies>(encoder: self)
return KeyedEncodingContainer(container)
}
@inlinable
func unkeyedContainer() -> UnkeyedEncodingContainer {
_StaticJSONUnkeyedContainer<Sink, Strategies>(encoder: self)
}
@inlinable
func singleValueContainer() -> SingleValueEncodingContainer {
_StaticJSONSingleValueContainer<Sink, Strategies>(encoder: self)
}
}
// MARK: - Keyed Container
@usableFromInline
internal struct _StaticJSONKeyedContainer<Key: CodingKey, Sink: JSONSink, Strategies: JSONEncodingStrategies>: KeyedEncodingContainerProtocol {
@usableFromInline let encoder: _StaticJSONEncoder<Sink, Strategies>
@usableFromInline var codingPath: [CodingKey] { encoder.state.codingPath }
@usableFromInline
init(encoder: _StaticJSONEncoder<Sink, Strategies>) {
self.encoder = encoder
encoder.state.sink.beginObject()
}
@inlinable
func encodeNil(forKey key: Key) throws {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
encoder.state.sink.writeNull()
}
@inlinable
func encode<T>(_ value: T, forKey key: Key) throws where T: Encodable {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
encoder.state.codingPath.append(key)
defer { encoder.state.codingPath.removeLast() }
// Check primitives first (most common case)
if let primitive = value as? _StaticJSONPrimitiveEncodable {
try primitive.encodeToJSON(sink: &encoder.state.sink, strategies: Strategies.self)
} else if let date = value as? Date {
let encoded = try Strategies.DateStrategy.encode(date)
encoder.state.sink.writeString(encoded)
} else if let data = value as? Data {
let encoded = try Strategies.DataStrategy.encode(data)
encoder.state.sink.writeString(encoded)
} else if let url = value as? URL {
encoder.state.sink.writeString(url.absoluteString)
} else {
// Fallback to container encoding
try value.encode(to: encoder)
}
}
// Specialized overloads for common types (no dynamic casts)
@inlinable
func encode(_ value: String, forKey key: Key) throws {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
encoder.state.sink.writeString(value)
}
@inlinable
func encode(_ value: Int, forKey key: Key) throws {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
encoder.state.sink.writeNumber(String(value))
}
@inlinable
func encode(_ value: Int8, forKey key: Key) throws {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
encoder.state.sink.writeNumber(String(value))
}
@inlinable
func encode(_ value: Int16, forKey key: Key) throws {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
encoder.state.sink.writeNumber(String(value))
}
@inlinable
func encode(_ value: Int32, forKey key: Key) throws {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
encoder.state.sink.writeNumber(String(value))
}
@inlinable
func encode(_ value: Bool, forKey key: Key) throws {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
encoder.state.sink.writeBool(value)
}
@inlinable
func encode(_ value: Double, forKey key: Key) throws {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
if !value.isFinite {
if Strategies.FloatingPointStrategy.throwOnNonConformingFloat {
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Non-conforming floating point value"))
} else {
encoder.state.sink.writeNumber(Strategies.FloatingPointStrategy.encodeNonConformingFloat(value))
}
} else {
encoder.state.sink.writeNumber(String(value))
}
}
@inlinable
func encode(_ value: Int64, forKey key: Key) throws {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
encoder.state.sink.writeNumber(String(value))
}
@inlinable
func encode(_ value: UInt, forKey key: Key) throws {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
encoder.state.sink.writeNumber(String(value))
}
@inlinable
func encode(_ value: UInt8, forKey key: Key) throws {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
encoder.state.sink.writeNumber(String(value))
}
@inlinable
func encode(_ value: UInt16, forKey key: Key) throws {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
encoder.state.sink.writeNumber(String(value))
}
@inlinable
func encode(_ value: UInt32, forKey key: Key) throws {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
encoder.state.sink.writeNumber(String(value))
}
@inlinable
func encode(_ value: UInt64, forKey key: Key) throws {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
encoder.state.sink.writeNumber(String(value))
}
@inlinable
func encode(_ value: Float, forKey key: Key) throws {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
let double = Double(value)
if !double.isFinite {
if Strategies.FloatingPointStrategy.throwOnNonConformingFloat {
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Non-conforming floating point value"))
} else {
encoder.state.sink.writeNumber(Strategies.FloatingPointStrategy.encodeNonConformingFloat(double))
}
} else {
encoder.state.sink.writeNumber(String(value))
}
}
@inlinable
func encode(_ value: Date, forKey key: Key) throws {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
let encoded = try Strategies.DateStrategy.encode(value)
encoder.state.sink.writeString(encoded)
}
@inlinable
func encode(_ value: Data, forKey key: Key) throws {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
let encoded = try Strategies.DataStrategy.encode(value)
encoder.state.sink.writeString(encoded)
}
@inlinable
func encode(_ value: URL, forKey key: Key) throws {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
encoder.state.sink.writeString(value.absoluteString)
}
// Specialized array overloads - bypass UnkeyedContainer protocol overhead
@inlinable
func encode(_ value: [Int], forKey key: Key) throws {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
encoder.state.sink.beginArray()
for element in value {
encoder.state.sink.writeNumber(String(element))
}
encoder.state.sink.endArray()
}
@inlinable
func encode(_ value: [String], forKey key: Key) throws {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
encoder.state.sink.beginArray()
for element in value {
encoder.state.sink.writeString(element)
}
encoder.state.sink.endArray()
}
@inlinable
func encode(_ value: [Double], forKey key: Key) throws {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
encoder.state.sink.beginArray()
for element in value {
if !element.isFinite {
if Strategies.FloatingPointStrategy.throwOnNonConformingFloat {
throw EncodingError.invalidValue(element, EncodingError.Context(codingPath: [], debugDescription: "Non-conforming floating point value"))
} else {
encoder.state.sink.writeNumber(Strategies.FloatingPointStrategy.encodeNonConformingFloat(element))
}
} else {
encoder.state.sink.writeNumber(String(element))
}
}
encoder.state.sink.endArray()
}
@inlinable
func encode(_ value: [Bool], forKey key: Key) throws {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
encoder.state.sink.beginArray()
for element in value {
encoder.state.sink.writeBool(element)
}
encoder.state.sink.endArray()
}
@inlinable
func nestedContainer<NestedKey>(
keyedBy keyType: NestedKey.Type,
forKey key: Key
) -> KeyedEncodingContainer<NestedKey> where NestedKey: CodingKey {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
encoder.state.codingPath.append(key)
let container = _StaticJSONKeyedContainer<NestedKey, Sink, Strategies>(encoder: encoder)
return KeyedEncodingContainer(container)
}
@inlinable
func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
encoder.state.codingPath.append(key)
return _StaticJSONUnkeyedContainer<Sink, Strategies>(encoder: encoder)
}
@inlinable
func superEncoder() -> Encoder {
let key = _StaticJSONKey(stringValue: "super")
encoder.state.sink.writeKey("super")
encoder.state.codingPath.append(key)
return encoder
}
@inlinable
func superEncoder(forKey key: Key) -> Encoder {
let transformedKey = Strategies.KeyTransform.transform(key.stringValue)
encoder.state.sink.writeKey(transformedKey)
encoder.state.codingPath.append(key)
return encoder
}
}
// MARK: - Unkeyed Container
@usableFromInline
internal struct _StaticJSONUnkeyedContainer<Sink: JSONSink, Strategies: JSONEncodingStrategies>: UnkeyedEncodingContainer {
@usableFromInline let encoder: _StaticJSONEncoder<Sink, Strategies>
@usableFromInline var codingPath: [CodingKey] { encoder.state.codingPath }
@usableFromInline var count: Int = 0
@usableFromInline
init(encoder: _StaticJSONEncoder<Sink, Strategies>) {
self.encoder = encoder
encoder.state.sink.beginArray()
}
@inlinable
mutating func encodeNil() throws {
encoder.state.sink.writeNull()
count += 1
}
@inlinable
mutating func encode<T>(_ value: T) throws where T: Encodable {
defer { count += 1 }
let key = _StaticJSONKey(intValue: count)
encoder.state.codingPath.append(key)
defer { encoder.state.codingPath.removeLast() }
// Check for less common primitive types (Int8, UInt*, etc.)
// Common types (Int, String, Bool, Date, Data, URL, arrays) are handled by specialized overloads
if let primitive = value as? _StaticJSONPrimitiveEncodable {
try primitive.encodeToJSON(sink: &encoder.state.sink, strategies: Strategies.self)
} else {
// Fallback to container encoding for custom types
try value.encode(to: encoder)
}
}
// Specialized overloads for common types (no dynamic casts)
@inlinable
mutating func encode(_ value: String) throws {
defer { count += 1 }
encoder.state.sink.writeString(value)
}
@inlinable
mutating func encode(_ value: Int) throws {
defer { count += 1 }
encoder.state.sink.writeNumber(String(value))
}
@inlinable
mutating func encode(_ value: Int8) throws {
defer { count += 1 }
encoder.state.sink.writeNumber(String(value))
}
@inlinable
mutating func encode(_ value: Int16) throws {
defer { count += 1 }
encoder.state.sink.writeNumber(String(value))
}
@inlinable
mutating func encode(_ value: Int32) throws {
defer { count += 1 }
encoder.state.sink.writeNumber(String(value))
}
@inlinable
mutating func encode(_ value: Bool) throws {
defer { count += 1 }
encoder.state.sink.writeBool(value)
}
@inlinable
mutating func encode(_ value: Double) throws {
defer { count += 1 }
if !value.isFinite {
if Strategies.FloatingPointStrategy.throwOnNonConformingFloat {
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Non-conforming floating point value"))
} else {
encoder.state.sink.writeNumber(Strategies.FloatingPointStrategy.encodeNonConformingFloat(value))
}
} else {
encoder.state.sink.writeNumber(String(value))
}
}
@inlinable
mutating func encode(_ value: Int64) throws {
defer { count += 1 }
encoder.state.sink.writeNumber(String(value))
}
@inlinable
mutating func encode(_ value: UInt) throws {
defer { count += 1 }
encoder.state.sink.writeNumber(String(value))
}
@inlinable
mutating func encode(_ value: UInt8) throws {
defer { count += 1 }
encoder.state.sink.writeNumber(String(value))
}
@inlinable
mutating func encode(_ value: UInt16) throws {
defer { count += 1 }
encoder.state.sink.writeNumber(String(value))
}
@inlinable
mutating func encode(_ value: UInt32) throws {
defer { count += 1 }
encoder.state.sink.writeNumber(String(value))
}
@inlinable
mutating func encode(_ value: UInt64) throws {
defer { count += 1 }
encoder.state.sink.writeNumber(String(value))
}
@inlinable
mutating func encode(_ value: Float) throws {
defer { count += 1 }
let double = Double(value)
if !double.isFinite {
if Strategies.FloatingPointStrategy.throwOnNonConformingFloat {
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Non-conforming floating point value"))
} else {
encoder.state.sink.writeNumber(Strategies.FloatingPointStrategy.encodeNonConformingFloat(double))
}
} else {
encoder.state.sink.writeNumber(String(value))
}
}
@inlinable
mutating func encode(_ value: Date) throws {
defer { count += 1 }
let encoded = try Strategies.DateStrategy.encode(value)
encoder.state.sink.writeString(encoded)
}
@inlinable
mutating func encode(_ value: Data) throws {
defer { count += 1 }
let encoded = try Strategies.DataStrategy.encode(value)
encoder.state.sink.writeString(encoded)
}
@inlinable
mutating func encode(_ value: URL) throws {
defer { count += 1 }
encoder.state.sink.writeString(value.absoluteString)
}
// Specialized array overloads - bypass UnkeyedContainer protocol overhead
@inlinable
mutating func encode(_ value: [Int]) throws {
defer { count += 1 }
encoder.state.sink.beginArray()
for element in value {
encoder.state.sink.writeNumber(String(element))
}
encoder.state.sink.endArray()
}
@inlinable
mutating func encode(_ value: [String]) throws {
defer { count += 1 }
encoder.state.sink.beginArray()
for element in value {
encoder.state.sink.writeString(element)
}
encoder.state.sink.endArray()
}
@inlinable
mutating func encode(_ value: [Double]) throws {
defer { count += 1 }
encoder.state.sink.beginArray()
for element in value {
if !element.isFinite {
if Strategies.FloatingPointStrategy.throwOnNonConformingFloat {
throw EncodingError.invalidValue(element, EncodingError.Context(codingPath: [], debugDescription: "Non-conforming floating point value"))
} else {
encoder.state.sink.writeNumber(Strategies.FloatingPointStrategy.encodeNonConformingFloat(element))
}
} else {
encoder.state.sink.writeNumber(String(element))
}
}
encoder.state.sink.endArray()
}
@inlinable
mutating func encode(_ value: [Bool]) throws {
defer { count += 1 }
encoder.state.sink.beginArray()
for element in value {
encoder.state.sink.writeBool(element)
}
encoder.state.sink.endArray()
}
@inlinable
mutating func nestedContainer<NestedKey>(
keyedBy keyType: NestedKey.Type
) -> KeyedEncodingContainer<NestedKey> where NestedKey: CodingKey {
let key = _StaticJSONKey(intValue: count)
count += 1
encoder.state.codingPath.append(key)
let container = _StaticJSONKeyedContainer<NestedKey, Sink, Strategies>(encoder: encoder)
return KeyedEncodingContainer(container)
}
@inlinable
mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
let key = _StaticJSONKey(intValue: count)
count += 1
encoder.state.codingPath.append(key)
return _StaticJSONUnkeyedContainer<Sink, Strategies>(encoder: encoder)
}
@inlinable
mutating func superEncoder() -> Encoder {
let key = _StaticJSONKey(intValue: count)
count += 1
encoder.state.codingPath.append(key)
return encoder
}
}
// MARK: - Single Value Container
@usableFromInline
internal struct _StaticJSONSingleValueContainer<Sink: JSONSink, Strategies: JSONEncodingStrategies>: SingleValueEncodingContainer {
@usableFromInline let encoder: _StaticJSONEncoder<Sink, Strategies>
@usableFromInline var codingPath: [CodingKey] { encoder.state.codingPath }
@usableFromInline
init(encoder: _StaticJSONEncoder<Sink, Strategies>) {
self.encoder = encoder
}
@inlinable
func encodeNil() throws {
encoder.state.sink.writeNull()
}
@inlinable
func encode<T>(_ value: T) throws where T: Encodable {
// Note: SingleValueEncodingContainer doesn't have specialized overloads,
// so we need these checks here. Most usage goes through keyed/unkeyed containers.
if let primitive = value as? _StaticJSONPrimitiveEncodable {
try primitive.encodeToJSON(sink: &encoder.state.sink, strategies: Strategies.self)
} else if let date = value as? Date {
let encoded = try Strategies.DateStrategy.encode(date)
encoder.state.sink.writeString(encoded)
} else if let data = value as? Data {
let encoded = try Strategies.DataStrategy.encode(data)
encoder.state.sink.writeString(encoded)
} else if let url = value as? URL {
encoder.state.sink.writeString(url.absoluteString)
} else {
// Fallback to container encoding
try value.encode(to: encoder)
}
}
}
// MARK: - Helper Types
@usableFromInline
internal struct _StaticJSONKey: CodingKey {
@usableFromInline var stringValue: String
@usableFromInline var intValue: Int?
@usableFromInline
init(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}
@usableFromInline
init(intValue: Int) {
self.stringValue = "\(intValue)"
self.intValue = intValue
}
}
@usableFromInline
internal protocol _StaticJSONPrimitiveEncodable {
func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>(
sink: inout Sink,
strategies: Strategies.Type
) throws
}
extension String: _StaticJSONPrimitiveEncodable {
@usableFromInline
func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>(
sink: inout Sink,
strategies: Strategies.Type
) throws {
sink.writeString(self)
}
}
extension Int: _StaticJSONPrimitiveEncodable {
@usableFromInline
func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>(
sink: inout Sink,
strategies: Strategies.Type
) throws {
sink.writeNumber(String(self))
}
}
extension Int8: _StaticJSONPrimitiveEncodable {
@usableFromInline
func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>(
sink: inout Sink,
strategies: Strategies.Type
) throws {
sink.writeNumber(String(self))
}
}
extension Int16: _StaticJSONPrimitiveEncodable {
@usableFromInline
func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>(
sink: inout Sink,
strategies: Strategies.Type
) throws {
sink.writeNumber(String(self))
}
}
extension Int32: _StaticJSONPrimitiveEncodable {
@usableFromInline
func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>(
sink: inout Sink,
strategies: Strategies.Type
) throws {
sink.writeNumber(String(self))
}
}
extension Int64: _StaticJSONPrimitiveEncodable {
@usableFromInline
func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>(
sink: inout Sink,
strategies: Strategies.Type
) throws {
sink.writeNumber(String(self))
}
}
extension UInt: _StaticJSONPrimitiveEncodable {
@usableFromInline
func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>(
sink: inout Sink,
strategies: Strategies.Type
) throws {
sink.writeNumber(String(self))
}
}
extension UInt8: _StaticJSONPrimitiveEncodable {
@usableFromInline
func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>(
sink: inout Sink,
strategies: Strategies.Type
) throws {
sink.writeNumber(String(self))
}
}
extension UInt16: _StaticJSONPrimitiveEncodable {
@usableFromInline
func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>(
sink: inout Sink,
strategies: Strategies.Type
) throws {
sink.writeNumber(String(self))
}
}
extension UInt32: _StaticJSONPrimitiveEncodable {
@usableFromInline
func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>(
sink: inout Sink,
strategies: Strategies.Type
) throws {
sink.writeNumber(String(self))
}
}
extension UInt64: _StaticJSONPrimitiveEncodable {
@usableFromInline
func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>(
sink: inout Sink,
strategies: Strategies.Type
) throws {
sink.writeNumber(String(self))
}
}
extension Float: _StaticJSONPrimitiveEncodable {
@usableFromInline
func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>(
sink: inout Sink,
strategies: Strategies.Type
) throws {
let double = Double(self)
if !double.isFinite {
if Strategies.FloatingPointStrategy.throwOnNonConformingFloat {
throw EncodingError.invalidValue(
self,
EncodingError.Context(
codingPath: [],
debugDescription: "Non-conforming floating point value"
)
)
} else {
sink.writeNumber(Strategies.FloatingPointStrategy.encodeNonConformingFloat(double))
}
} else {
sink.writeNumber(String(self))
}
}
}
extension Double: _StaticJSONPrimitiveEncodable {
@usableFromInline
func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>(
sink: inout Sink,
strategies: Strategies.Type
) throws {
if !self.isFinite {
if Strategies.FloatingPointStrategy.throwOnNonConformingFloat {
throw EncodingError.invalidValue(
self,
EncodingError.Context(
codingPath: [],
debugDescription: "Non-conforming floating point value"
)
)
} else {
sink.writeNumber(Strategies.FloatingPointStrategy.encodeNonConformingFloat(self))
}
} else {
sink.writeNumber(String(self))
}
}
}
extension Bool: _StaticJSONPrimitiveEncodable {
@usableFromInline
func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>(
sink: inout Sink,
strategies: Strategies.Type
) throws {
sink.writeBool(self)
}
}
import Testing
import Foundation
import StaticEncoder
@Suite("Static JSON Encoder Tests")
struct StaticJSONEncoderTests {
// MARK: - Simple Model Tests
@Test("JSON encoder encodes simple model")
func simpleModel() throws {
struct Person: Encodable {
let name: String
let age: Int
}
let encoder = StaticJSONEncoder<JSONDataSink, StandardJSONEncodingStrategies>()
let person = Person(name: "Alice", age: 30)
let data = try encoder.encode(person)
let json = String(data: data, encoding: .utf8)!
#expect(json.contains("\"name\""))
#expect(json.contains("\"Alice\""))
#expect(json.contains("\"age\""))
#expect(json.contains("30"))
}
@Test("JSON encoder encodes nested model")
func nestedModel() throws {
struct Address: Encodable {
let street: String
let city: String
}
struct Person: Encodable {
let name: String
let address: Address
}
let encoder = StaticJSONEncoder<JSONDataSink, StandardJSONEncodingStrategies>()
let person = Person(
name: "Bob",
address: Address(street: "Main St", city: "NYC")
)
let data = try encoder.encode(person)
let json = String(data: data, encoding: .utf8)!
#expect(json.contains("\"name\""))
#expect(json.contains("\"Bob\""))
#expect(json.contains("\"address\""))
#expect(json.contains("\"street\""))
#expect(json.contains("\"Main St\""))
#expect(json.contains("\"city\""))
#expect(json.contains("\"NYC\""))
}
@Test("JSON encoder encodes array")
func arrayEncoding() throws {
struct Model: Encodable {
let tags: [String]
}
let encoder = StaticJSONEncoder<JSONDataSink, StandardJSONEncodingStrategies>()
let model = Model(tags: ["swift", "ios", "macos"])
let data = try encoder.encode(model)
let json = String(data: data, encoding: .utf8)!
#expect(json.contains("\"tags\""))
#expect(json.contains("["))
#expect(json.contains("\"swift\""))
#expect(json.contains("\"ios\""))
#expect(json.contains("\"macos\""))
}
// MARK: - Primitive Type Tests
@Test("JSON encoder handles all primitive types")
func primitiveTypes() throws {
struct AllTypes: Encodable {
let string: String
let int: Int
let double: Double
let bool: Bool
}
let encoder = StaticJSONEncoder<JSONDataSink, StandardJSONEncodingStrategies>()
let model = AllTypes(
string: "test",
int: 42,
double: 3.14,
bool: true
)
let data = try encoder.encode(model)
let json = String(data: data, encoding: .utf8)!
#expect(json.contains("\"string\""))
#expect(json.contains("\"test\""))
#expect(json.contains("\"int\""))
#expect(json.contains("42"))
#expect(json.contains("\"double\""))
#expect(json.contains("3.14"))
#expect(json.contains("\"bool\""))
#expect(json.contains("true"))
}
// MARK: - Snake Case Strategy Tests
@Test("Snake case strategy transforms keys")
func snakeCaseStrategy() throws {
struct Person: Encodable {
let firstName: String
let lastName: String
}
let encoder = StaticJSONEncoder<JSONDataSink, SnakeCaseJSONEncodingStrategies>()
let person = Person(firstName: "John", lastName: "Doe")
let data = try encoder.encode(person)
let json = String(data: data, encoding: .utf8)!
#expect(json.contains("\"first_name\""))
#expect(json.contains("\"last_name\""))
#expect(!json.contains("firstName"))
#expect(!json.contains("lastName"))
}
// MARK: - Data Sink Tests
@Test("JSON Data sink produces valid JSON")
func dataSink() throws {
struct Model: Encodable {
let value: Int
}
let encoder = StaticJSONEncoder<JSONDataSink, StandardJSONEncodingStrategies>()
let model = Model(value: 123)
let data = try encoder.encode(model)
let string = String(data: data, encoding: .utf8)
#expect(string != nil)
#expect(string!.contains("\"value\""))
#expect(string!.contains("123"))
}
// MARK: - Performance Tests
@Test("StaticJSONEncoder performance", .timeLimit(.minutes(1)))
func encodingPerformanceIntoString() throws {
@inline(never)
@_optimize(none)
func blackHole<T>(_ x: T) {
withUnsafePointer(to: x) { ptr in
_ = ptr.pointee
}
}
struct Model: Encodable {
let id: Int
let name: String
let tags: [String]
let metadata: Metadata
struct Metadata: Encodable {
let created: String
let updated: String
}
}
let encoder = StaticJSONEncoder<JSONDataSink, StandardJSONEncodingStrategies>()
for i in 0..<1_000_000 {
let model = Model(
id: i,
name: "Test",
tags: ["swift", "ios", "macos"],
metadata: .init(created: "2024", updated: "2025")
)
let output = try encoder.encode(model)
blackHole(output)
}
}
@Test("Foundation JSON Encoder performance", .timeLimit(.minutes(1)))
func FoundationJSONEncoderEncodingPerformance() throws {
@inline(never)
@_optimize(none)
func blackHole<T>(_ x: T) {
withUnsafePointer(to: x) { ptr in
_ = ptr.pointee
}
}
struct Model: Encodable {
let id: Int
let name: String
let tags: [String]
let metadata: Metadata
struct Metadata: Encodable {
let created: String
let updated: String
}
}
let encoder = Foundation.JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.keyEncodingStrategy = .useDefaultKeys
encoder.dataEncodingStrategy = .base64
encoder.nonConformingFloatEncodingStrategy = .throw
for i in 0..<1_000_000 {
let model = Model(
id: i,
name: "Test",
tags: ["swift", "ios", "macos"],
metadata: .init(created: "2024", updated: "2025")
)
let output = try encoder.encode(model)
blackHole(output)
}
}
// MARK: - Type Erasure Tests
@Test("Type-erased JSON encoder works")
func typeErasure() throws {
struct Model: Encodable {
let value: String
}
let encoder = StaticJSONEncoder<JSONDataSink, StandardJSONEncodingStrategies>()
.eraseToAnyEncoder()
let model = Model(value: "test")
let data: Data = try encoder.encode(model)
let json = String(data: data, encoding: .utf8)!
#expect(json.contains("\"value\""))
#expect(json.contains("\"test\""))
}
}
// MARK: - Convenient Type Aliases
/// Standard query string encoder with brackets notation.
///
/// Equivalent to `StaticURLParameterEncoder<Sink, BracketsEncodingStrategies>`
///
/// - Arrays: `items[]=1&items[]=2`
/// - Objects: `user[name]=Ana`
/// - Dates: ISO8601
public typealias StaticQueryEncoder<Sink: URLParameterSink> =
StaticURLParameterEncoder<Sink, BracketsEncodingStrategies>
/// URL template encoder with dot notation.
///
/// Equivalent to `StaticURLParameterEncoder<Sink, DotNotationEncodingStrategies>`
///
/// - Arrays: `items[]=1&items[]=2`
/// - Objects: `user.name=Ana` (dot notation)
/// - Dates: ISO8601
public typealias StaticTemplateEncoder<Sink: URLParameterSink> =
StaticURLParameterEncoder<Sink, DotNotationEncodingStrategies>
/// Query encoder with snake_case key transformation.
///
/// Equivalent to `StaticURLParameterEncoder<Sink, SnakeCaseEncodingStrategies>`
///
/// - Arrays: `items[]=1&items[]=2`
/// - Objects: `user[home_address]=value`
/// - Keys: Transformed to snake_case
/// - Dates: ISO8601
public typealias StaticSnakeCaseQueryEncoder<Sink: URLParameterSink> =
StaticURLParameterEncoder<Sink, SnakeCaseEncodingStrategies>
/// Template encoder with dot notation and snake_case keys.
///
/// Equivalent to `StaticURLParameterEncoder<Sink, DotNotationSnakeCaseStrategies>`
///
/// Common for REST APIs.
public typealias StaticRESTEncoder<Sink: URLParameterSink> =
StaticURLParameterEncoder<Sink, DotNotationSnakeCaseStrategies>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment