import Foundation
/// A JSON document as a value: the escape hatch for untyped access (`client.raw`),
/// error bodies, and the `last` payload of a timeout. Objects are unordered.
public indirect enum JSONValue: Sendable, Hashable {
case null
case bool(Bool)
case number(Double)
case string(String)
case array([JSONValue])
case object([String: JSONValue])
}
extension JSONValue: Codable {
public init(from decoder: any Decoder) throws {
let c = try decoder.singleValueContainer()
if c.decodeNil() { self = .null; return }
if let b = try? c.decode(Bool.self) { self = .bool(b); return }
if let d = try? c.decode(Double.self) { self = .number(d); return }
if let s = try? c.decode(String.self) { self = .string(s); return }
if let a = try? c.decode([JSONValue].self) { self = .array(a); return }
if let o = try? c.decode([String: JSONValue].self) { self = .object(o); return }
throw DecodingError.dataCorruptedError(in: c, debugDescription: "not a JSON value")
}
public func encode(to encoder: any Encoder) throws {
var c = encoder.singleValueContainer()
switch self {
case .null: try c.encodeNil()
case .bool(let b): try c.encode(b)
case .number(let d):
if d.rounded() == d, abs(d) < 9_007_199_254_740_992 { try c.encode(Int64(d)) } else { try c.encode(d) }
case .string(let s): try c.encode(s)
case .array(let a): try c.encode(a)
case .object(let o): try c.encode(o)
}
}
}
extension JSONValue: ExpressibleByNilLiteral, ExpressibleByBooleanLiteral, ExpressibleByIntegerLiteral,
ExpressibleByFloatLiteral, ExpressibleByStringLiteral, ExpressibleByArrayLiteral, ExpressibleByDictionaryLiteral {
public init(nilLiteral: ()) { self = .null }
public init(booleanLiteral value: Bool) { self = .bool(value) }
public init(integerLiteral value: Int) { self = .number(Double(value)) }
public init(floatLiteral value: Double) { self = .number(value) }
public init(stringLiteral value: String) { self = .string(value) }
public init(arrayLiteral elements: JSONValue...) { self = .array(elements) }
public init(dictionaryLiteral elements: (String, JSONValue)...) {
self = .object(Dictionary(elements, uniquingKeysWith: { _, last in last }))
}
}
public extension JSONValue {
/// Parses JSON bytes (any top-level value).
static func parse(_ data: Data) throws -> JSONValue {
try JSONDecoder().decode(JSONValue.self, from: data)
}
/// Encodes any `Encodable` and re-reads it as a value.
init<T: Encodable>(encoding value: T) throws {
self = try JSONValue.parse(try JSONEncoder().encode(value))
}
/// Decodes this value into a typed model.
func decode<T: Decodable>(_ type: T.Type) throws -> T {
try JSONDecoder().decode(type, from: try serialized())
}
/// Serializes to compact JSON with sorted keys.
func serialized() throws -> Data {
let e = JSONEncoder()
e.outputFormatting = [.sortedKeys]
return try e.encode(self)
}
var isNull: Bool { if case .null = self { return true }; return false }
var boolValue: Bool? { if case .bool(let b) = self { return b }; return nil }
var doubleValue: Double? { if case .number(let d) = self { return d }; return nil }
var intValue: Int? {
guard case .number(let d) = self, d.rounded() == d, let i = Int(exactly: d) else { return nil }
return i
}
var stringValue: String? { if case .string(let s) = self { return s }; return nil }
var arrayValue: [JSONValue]? { if case .array(let a) = self { return a }; return nil }
var objectValue: [String: JSONValue]? { if case .object(let o) = self { return o }; return nil }
subscript(key: String) -> JSONValue? { objectValue?[key] }
subscript(index: Int) -> JSONValue? {
guard let a = arrayValue, a.indices.contains(index) else { return nil }
return a[index]
}
/// Follows a key path of object keys; nil when any hop is missing.
func at(_ path: [String]) -> JSONValue? {
var v: JSONValue? = self
for k in path { v = v?[k] }
return v
}
}