Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ Portions of this project incorporate code from the following source code or test

* [swiftlang/swift-foundation](https://github.com/swiftlang/swift-foundation), licensed under the Apache License, Version 2.0.
* [michaeleisel/ZippyJSON](https://github.com/michaeleisel/ZippyJSON), licensed under the MIT License.
* [mattt/swift-yyjson](https://github.com/mattt/swift-yyjson), licensed under the MIT License. The `ReerJSONSerialization`, `Value`, `Configuration`, `Error`, and `Helpers` modules are adapted from this project.

See the LICENSE file for the full text of both licenses.

Expand All @@ -107,6 +108,7 @@ We would like to express our gratitude to the following projects and their contr
* **[swiftlang/swift-foundation](https://github.com/swiftlang/swift-foundation)** - For implementation reference and comprehensive test suites that helped ensure compatibility.
* **[michaeleisel/ZippyJSON](https://github.com/michaeleisel/ZippyJSON)** - For the innovative Swift JSON parsing approach and valuable test cases.
* **[michaeleisel/JJLISO8601DateFormatter](https://github.com/michaeleisel/JJLISO8601DateFormatter)** - For the high-performance date formatting implementation.
* **[mattt/swift-yyjson](https://github.com/mattt/swift-yyjson)** - For the `JSONSerialization` replacement and DOM-style `JSONValue`/`JSONDocument` APIs. The `ReerJSONSerialization`, `Value`, `Configuration`, `Error`, and `Helpers` source files and their tests are adapted from this project.
* **[nixzhu/Ananda](https://github.com/nixzhu/Ananda)** - For the pioneering work in integrating yyjson with Swift and providing architectural inspiration.

Special thanks to all the open-source contributors who made this project possible.
153 changes: 153 additions & 0 deletions Sources/ReerJSON/Configuration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//
// Adapted from swift-yyjson by Mattt (https://github.com/mattt/swift-yyjson)
// Original code copyright 2026 Mattt (https://mat.tt), licensed under MIT License.
//
// Modifications for ReerJSON:
// - Renamed types: removed "YY" prefix (YYJSONValue → JSONValue, etc.)
// - YYJSONSerialization → ReerJSONSerialization
// - Changed `import Cyyjson` to `import yyjson`
//
// 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 yyjson
import Foundation

/// Options for reading JSON data.
public struct JSONReadOptions: OptionSet, Sendable {
public let rawValue: UInt32

public init(rawValue: UInt32) {
self.rawValue = rawValue
}

/// Default option (RFC 8259 compliant).
public static let `default` = JSONReadOptions([])

/// Stops when done instead of issuing an error if there's additional content
/// after a JSON document.
public static let stopWhenDone = JSONReadOptions(rawValue: YYJSON_READ_STOP_WHEN_DONE)

/// Read all numbers as raw strings.
public static let numberAsRaw = JSONReadOptions(rawValue: YYJSON_READ_NUMBER_AS_RAW)

/// Allow reading invalid unicode when parsing string values.
public static let allowInvalidUnicode = JSONReadOptions(rawValue: YYJSON_READ_ALLOW_INVALID_UNICODE)

/// Read big numbers as raw strings.
public static let bigNumberAsRaw = JSONReadOptions(rawValue: YYJSON_READ_BIGNUM_AS_RAW)

#if !YYJSON_DISABLE_NON_STANDARD

/// Allow single trailing comma at the end of an object or array.
public static let allowTrailingCommas = JSONReadOptions(rawValue: YYJSON_READ_ALLOW_TRAILING_COMMAS)

/// Allow C-style single-line and multi-line comments.
public static let allowComments = JSONReadOptions(rawValue: YYJSON_READ_ALLOW_COMMENTS)

/// Allow inf/nan number and literal, case-insensitive.
public static let allowInfAndNaN = JSONReadOptions(rawValue: YYJSON_READ_ALLOW_INF_AND_NAN)

/// Allow UTF-8 BOM and skip it before parsing.
public static let allowBOM = JSONReadOptions(rawValue: YYJSON_READ_ALLOW_BOM)

/// Allow extended number formats (hex, leading/trailing decimal point, leading plus).
public static let allowExtendedNumbers = JSONReadOptions(rawValue: YYJSON_READ_ALLOW_EXT_NUMBER)

/// Allow extended escape sequences in strings.
public static let allowExtendedEscapes = JSONReadOptions(rawValue: YYJSON_READ_ALLOW_EXT_ESCAPE)

/// Allow extended whitespace characters.
public static let allowExtendedWhitespace = JSONReadOptions(rawValue: YYJSON_READ_ALLOW_EXT_WHITESPACE)

/// Allow strings enclosed in single quotes.
public static let allowSingleQuotedStrings = JSONReadOptions(rawValue: YYJSON_READ_ALLOW_SINGLE_QUOTED_STR)

/// Allow object keys without quotes.
public static let allowUnquotedKeys = JSONReadOptions(rawValue: YYJSON_READ_ALLOW_UNQUOTED_KEY)

/// Allow JSON5 format.
///
/// This includes trailing commas, comments, inf/nan, extended numbers,
/// extended escapes, extended whitespace, single-quoted strings, and unquoted keys.
public static let json5 = JSONReadOptions(rawValue: YYJSON_READ_JSON5)

#endif // !YYJSON_DISABLE_NON_STANDARD

/// Convert to yyjson read flags.
internal var yyjsonFlags: yyjson_read_flag {
yyjson_read_flag(rawValue)
}
}

/// Options for writing JSON data.
public struct JSONWriteOptions: OptionSet, Sendable {
public let rawValue: UInt32

public init(rawValue: UInt32) {
self.rawValue = rawValue
}

/// Default option (minified output).
public static let `default` = JSONWriteOptions([])

/// Write JSON pretty with 4 space indent.
public static let prettyPrinted = JSONWriteOptions(rawValue: YYJSON_WRITE_PRETTY)

/// Write JSON pretty with 2 space indent (implies `prettyPrinted`).
public static let indentationTwoSpaces = JSONWriteOptions(rawValue: YYJSON_WRITE_PRETTY_TWO_SPACES)

/// Escape unicode as `\uXXXX`, making the output ASCII only.
public static let escapeUnicode = JSONWriteOptions(rawValue: YYJSON_WRITE_ESCAPE_UNICODE)

/// Escape '/' as '\/'.
public static let escapeSlashes = JSONWriteOptions(rawValue: YYJSON_WRITE_ESCAPE_SLASHES)

#if !YYJSON_DISABLE_NON_STANDARD

/// Writes infinity and NaN values as `Infinity` and `NaN` literals.
///
/// If you set `infAndNaNAsNull`, it takes precedence.
public static let allowInfAndNaN = JSONWriteOptions(rawValue: YYJSON_WRITE_ALLOW_INF_AND_NAN)

/// Writes infinity and NaN values as `null` literals.
///
/// This option takes precedence over `allowInfAndNaN`.
public static let infAndNaNAsNull = JSONWriteOptions(rawValue: YYJSON_WRITE_INF_AND_NAN_AS_NULL)

#endif // !YYJSON_DISABLE_NON_STANDARD

/// Allow invalid unicode when encoding string values.
public static let allowInvalidUnicode = JSONWriteOptions(rawValue: YYJSON_WRITE_ALLOW_INVALID_UNICODE)

/// Add a newline character at the end of the JSON.
public static let newlineAtEnd = JSONWriteOptions(rawValue: YYJSON_WRITE_NEWLINE_AT_END)

/// Sorts object keys lexicographically.
public static let sortedKeys = JSONWriteOptions(rawValue: 1 << 16)

// Mask for Swift-only flags (bits 16+) that should not be passed to yyjson C library
private static let swiftOnlyFlagsMask: UInt32 = 0xFFFF_0000

/// Convert to yyjson write flags, excluding Swift-only flags.
internal var yyjsonFlags: yyjson_write_flag {
// Only pass bits 0-15 to yyjson C library; bits 16+ are Swift-only flags
yyjson_write_flag(rawValue & ~JSONWriteOptions.swiftOnlyFlagsMask)
}
}
175 changes: 175 additions & 0 deletions Sources/ReerJSON/Error.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
//
// Adapted from swift-yyjson by Mattt (https://github.com/mattt/swift-yyjson)
// Original code copyright 2026 Mattt (https://mat.tt), licensed under MIT License.
//
// Modifications for ReerJSON:
// - Renamed types: removed "YY" prefix (YYJSONValue → JSONValue, etc.)
// - YYJSONSerialization → ReerJSONSerialization
// - Changed `import Cyyjson` to `import yyjson`
//
// 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 yyjson
import Foundation

/// Errors that can occur when parsing, decoding, or encoding JSON.
public struct JSONError: Error, Equatable, Sendable, CustomStringConvertible {
/// The kind of error that occurred.
public enum Kind: Equatable, Sendable {
/// The JSON data was malformed.
case invalidJSON
/// The value was not of the expected type.
case typeMismatch(expected: String, actual: String)
/// A required key was not found.
case missingKey(String)
/// A required value was null or missing.
case missingValue
/// The data is corrupted or invalid.
case invalidData
/// An error occurred while writing JSON.
case writeError
}

/// The kind of error.
public let kind: Kind

/// A detailed message describing the error.
public let message: String

/// The coding path where the error occurred (for decoding errors).
public let path: String

public var description: String {
if path.isEmpty {
return message
}
return "\(message) (at \(path))"
}

private init(kind: Kind, message: String, path: String = "") {
self.kind = kind
self.message = message
self.path = path
}

// MARK: - Public Factory Methods

/// Create an invalid JSON error.
public static func invalidJSON(_ message: String) -> JSONError {
JSONError(kind: .invalidJSON, message: message)
}

/// Create a type mismatch error.
public static func typeMismatch(expected: String, actual: String, path: String = "") -> JSONError {
JSONError(
kind: .typeMismatch(expected: expected, actual: actual),
message: "Expected \(expected), got \(actual)",
path: path
)
}

/// Create a missing key error.
public static func missingKey(_ key: String, path: String = "") -> JSONError {
JSONError(
kind: .missingKey(key),
message: "Missing key '\(key)'",
path: path
)
}

/// Create a missing value error.
public static func missingValue(path: String = "") -> JSONError {
JSONError(
kind: .missingValue,
message: "Value is null or missing",
path: path
)
}

/// Create an invalid data error.
public static func invalidData(_ message: String, path: String = "") -> JSONError {
JSONError(kind: .invalidData, message: message, path: path)
}

/// Create a write error.
public static func writeError(_ message: String) -> JSONError {
JSONError(kind: .writeError, message: message)
}

// MARK: - Internal Initializers

/// Create an error from a yyjson read error.
internal init(parsing error: yyjson_read_err) {
let message: String
switch error.code {
case YYJSON_READ_ERROR_INVALID_PARAMETER:
message = "Invalid parameter"
case YYJSON_READ_ERROR_MEMORY_ALLOCATION:
message = "Memory allocation failed"
case YYJSON_READ_ERROR_EMPTY_CONTENT:
message = "Empty content"
case YYJSON_READ_ERROR_UNEXPECTED_CONTENT:
message = "Unexpected content"
case YYJSON_READ_ERROR_UNEXPECTED_END:
message = "Unexpected end of input"
case YYJSON_READ_ERROR_UNEXPECTED_CHARACTER:
message = "Unexpected character at position \(error.pos)"
case YYJSON_READ_ERROR_JSON_STRUCTURE:
message = "Invalid JSON structure"
case YYJSON_READ_ERROR_INVALID_COMMENT:
message = "Invalid comment"
case YYJSON_READ_ERROR_INVALID_NUMBER:
message = "Invalid number"
case YYJSON_READ_ERROR_INVALID_STRING:
message = "Invalid string"
case YYJSON_READ_ERROR_LITERAL:
message = "Invalid literal"
default:
message = "Unknown read error (code: \(error.code))"
}

self.kind = .invalidJSON
self.message = message
self.path = ""
}

/// Create an error from a yyjson write error.
internal init(writing error: yyjson_write_err) {
let message: String
switch error.code {
case YYJSON_WRITE_ERROR_INVALID_PARAMETER:
message = "Invalid parameter"
case YYJSON_WRITE_ERROR_MEMORY_ALLOCATION:
message = "Memory allocation failed"
case YYJSON_WRITE_ERROR_INVALID_VALUE_TYPE:
message = "Invalid value type"
case YYJSON_WRITE_ERROR_NAN_OR_INF:
message = "NaN or Infinity not allowed in JSON"
case YYJSON_WRITE_ERROR_INVALID_STRING:
message = "Invalid string"
default:
message = "Unknown write error (code: \(error.code))"
}

self.kind = .writeError
self.message = message
self.path = ""
}
}
Loading