@@ -66,7 +66,7 @@ enum JSONValue: Codable, Hashable, Sendable, Comparable {
6666 case .null: return "null"
6767 case .bool(let v): return v ? "true" : "false"
6868 case .int(let v): return String(v)
69− case .double(let v): return String(v)
69+ case .double(let v): return Self.jsonNumber(v)
7070 case .string(let v): return "\"\(Self.escapeJSONString(v))\""
7171 case .array(let items):
7272 return "[" + items.map { $0.compactJSON }.joined(separator: ",") + "]"
@@ -96,7 +96,7 @@ enum JSONValue: Codable, Hashable, Sendable, Comparable {
9696 case .null: return "NULL"
9797 case .bool(let v): return v ? "true" : "false"
9898 case .int(let v): return String(v)
99− case .double(let v): return String(v)
99+ case .double(let v): return v.isFinite ? String(v) : "NULL"
100100 case .string(let v): return "'\(v.replacingOccurrences(of: "'", with: "''"))'"
101101 case .array, .object:
102102 return "'\(exportString.replacingOccurrences(of: "'", with: "''"))'"
@@ -137,7 +137,7 @@ enum JSONValue: Codable, Hashable, Sendable, Comparable {
137137 case .null: return "null"
138138 case .bool(let v): return v ? "true" : "false"
139139 case .int(let v): return String(v)
140− case .double(let v): return String(v)
140+ case .double(let v): return Self.jsonNumber(v)
141141 case .string(let v): return "\"\(Self.escapeJSONString(v))\""
142142 case .array(let items):
143143 if items.isEmpty { return "[]" }
@@ -182,23 +182,43 @@ enum JSONValue: Codable, Hashable, Sendable, Comparable {
182182 return result
183183 }
184184
185+ /// Stable ordering bucket per case so cross-type comparisons form a total
186+ /// order. Integers and doubles share a bucket so they compare numerically.
187+ private var typeRank: Int {
188+ switch self {
189+ case .null: return 0
190+ case .bool: return 1
191+ case .int, .double: return 2
192+ case .string: return 3
193+ case .array: return 4
194+ case .object: return 5
195+ }
196+ }
197+
185198 /// Type-aware comparison: numbers compare numerically, strings
186199 /// lexicographically, nulls sort first, bools sort false < true.
187− /// Cross-type comparisons fall back to displayString.
200+ /// Different types are ordered by a stable type rank so that `<` is a strict
201+ /// weak ordering consistent with `==` (the `Comparable` contract requires
202+ /// that order-equivalent values are equal — a `displayString` fallback
203+ /// violated this, e.g. `.int(5)` vs `.string("5")` both render "5").
188204 static func < (lhs: JSONValue, rhs: JSONValue) -> Bool {
205+ if lhs.typeRank != rhs.typeRank { return lhs.typeRank < rhs.typeRank }
189206 switch (lhs, rhs) {
190207 case (.null, .null): return false
191− case (.null, _): return true
192− case (_, .null): return false
193208 case (.bool(let a), .bool(let b)): return !a && b
194209 case (.int(let a), .int(let b)): return a < b
195210 case (.double(let a), .double(let b)): return doubleLessThan(a, b)
196− case (.int(let a), .double(let b)): return doubleLessThan(Double(a), b)
197− case (.double(let a), .int(let b)): return doubleLessThan(a, Double(b))
211+ case (.int(let a), .double(let b)): return intLessThanDouble(a, b)
212+ case (.double(let a), .int(let b)): return doubleLessThanInt(a, b)
198213 case (.string(let a), .string(let b)):
199214 return a.localizedStandardCompare(b) == .orderedAscending
215+ case (.array, .array), (.object, .object):
216+ // Same rank, structured: equal values must not be ordered; otherwise
217+ // use a deterministic serialization tiebreaker.
218+ if lhs == rhs { return false }
219+ return lhs.compactJSON < rhs.compactJSON
200220 default:
201− return lhs.displayString < rhs.displayString
221+ return false
202222 }
203223 }
204224
@@ -211,6 +231,36 @@ enum JSONValue: Codable, Hashable, Sendable, Comparable {
211231 return a < b
212232 }
213233
234+ /// Exact `Int < Double` comparison that avoids the precision loss of
235+ /// `Double(Int)` for magnitudes beyond 2^53. NaN sorts after all numbers.
236+ private static func intLessThanDouble(_ a: Int, _ b: Double) -> Bool {
237+ if b.isNaN { return true }
238+ if let bi = Int(exactly: b) { return a < bi } // b is a whole, in-range number
239+ return Double(a) < b // b is fractional / infinite / out of range
240+ }
241+
242+ /// Exact `Double < Int` comparison, the mirror of `intLessThanDouble`.
243+ /// NaN sorts after all numbers, so `NaN < anyInt` is false.
244+ private static func doubleLessThanInt(_ a: Double, _ b: Int) -> Bool {
245+ if a.isNaN { return false }
246+ if let ai = Int(exactly: a) { return ai < b } // a is a whole, in-range number
247+ return a < Double(b) // a is fractional / infinite / out of range
248+ }
249+
250+ /// Exact `Int == Double` test (true only when the double represents exactly
251+ /// the same integer — `Int(exactly:)` is nil for fractional/out-of-range b).
252+ private static func intEqualsDouble(_ a: Int, _ b: Double) -> Bool {
253+ if let bi = Int(exactly: b) { return a == bi }
254+ return false
255+ }
256+
257+ /// JSON-safe rendering of a double: JSON has no NaN/Infinity literals, so
258+ /// non-finite values serialize as `null` rather than the invalid `nan`/`inf`
259+ /// that `String(_:)` would emit.
260+ static func jsonNumber(_ v: Double) -> String {
261+ v.isFinite ? String(v) : "null"
262+ }
263+
214264 // MARK: - Equatable / Hashable
215265
216266 /// Equality treats numerically-equal integers and doubles as equal
@@ -223,8 +273,8 @@ enum JSONValue: Codable, Hashable, Sendable, Comparable {
223273 case (.bool(let a), .bool(let b)): return a == b
224274 case (.int(let a), .int(let b)): return a == b
225275 case (.double(let a), .double(let b)): return a == b
226− case (.int(let a), .double(let b)): return Double(a) == b
227− case (.double(let a), .int(let b)): return a == Double(b)
276+ case (.int(let a), .double(let b)): return Self.intEqualsDouble(a, b)
277+ case (.double(let a), .int(let b)): return Self.intEqualsDouble(b, a)
228278 case (.string(let a), .string(let b)): return a == b
229279 case (.array(let a), .array(let b)): return a == b
230280 case (.object(let a), .object(let b)): return a == b