Skip to content

Commit e5ef310

Browse files
fredrikekreclaude
andauthored
vendor/jsonx: improve correctness and performance (#470)
- skip_whitespace: compare bytes directly against the 4 RFC 8259 whitespace characters instead of isspace(Char(...)). ~2x faster and correctly rejects vertical tab / form feed which isspace accepts but JSON does not. - parse_number: replace try/catch with tryparse — ~500x faster on the happy path, ~1400x on Int64 overflow. Also reject leading '+' which is not valid per the JSON spec. - parse_array: use explicit Any[] instead of untyped []. - tests: add cases for leading '+', vertical tab, and form feed. Co-authored-by: Claude Code <noreply@anthropic.com>
1 parent e346208 commit e5ef310

2 files changed

Lines changed: 19 additions & 15 deletions

File tree

vendor/jsonx.jl

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ end
4242

4343
# Helper function to skip whitespace
4444
function skip_whitespace(str::String, pos::Int, len::Int)
45-
while pos <= len && isspace(Char(codeunit(str, pos)))
45+
while pos <= len
46+
c = codeunit(str, pos)
47+
# JSON whitespace (RFC 8259): space, tab, newline, carriage return
48+
(c == 0x20 || c == 0x09 || c == 0x0A || c == 0x0D) || break
4649
pos += 1
4750
end
4851
return pos
@@ -206,7 +209,7 @@ end
206209
function parse_array(str::String, pos::Int, len::Int)
207210
codeunit(str, pos) != UInt8('[') && throw(ArgumentError("Expected '[' at position $pos"))
208211
pos += 1
209-
result = []
212+
result = Any[]
210213
pos = skip_whitespace(str, pos, len)
211214
pos > len && throw(ArgumentError("Unexpected end of input in array"))
212215
codeunit(str, pos) == UInt8(']') && return result, pos + 1
@@ -259,7 +262,9 @@ function parse_number(str::String, pos::Int, len::Int)
259262
has_decimal_or_exp = false
260263
while pos <= len
261264
c = codeunit(str, pos)
262-
if c == UInt8('-') || (UInt8('0') <= c <= UInt8('9')) || c == UInt8('+')
265+
if c == UInt8('-') || (UInt8('0') <= c <= UInt8('9'))
266+
pos += 1
267+
elseif c == UInt8('+') && pos > start_pos # '+' only valid after e/E
263268
pos += 1
264269
elseif c == UInt8('.') || c == UInt8('e') || c == UInt8('E')
265270
has_decimal_or_exp = true
@@ -270,21 +275,14 @@ function parse_number(str::String, pos::Int, len::Int)
270275
end
271276
num_str = @view str[start_pos:pos-1]
272277

273-
# Try parsing as Int64 if no decimal point or exponent
274278
if !has_decimal_or_exp
275-
try
276-
return Base.parse(Int64, num_str), pos
277-
catch
278-
# Fall back to Float64 if Int64 parsing fails (e.g., overflow)
279-
end
279+
n = tryparse(Int64, num_str)
280+
n !== nothing && return n, pos
280281
end
281282

282-
# Parse as Float64
283-
try
284-
return Base.parse(Float64, num_str), pos
285-
catch
286-
throw(ArgumentError("Invalid number format"))
287-
end
283+
f = tryparse(Float64, num_str)
284+
f !== nothing && return f, pos
285+
throw(ArgumentError("Invalid number format: $num_str"))
288286
end
289287

290288
# JSON writing functionality

vendor/test.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ include("jsonx.jl")
9090
@test JSONX.parse("\t\n\r null \t\n\r") === nothing
9191
@test JSONX.parse("[ 1 , 2 , 3 ]") == [1, 2, 3]
9292
@test JSONX.parse("{ \"key\" : \"value\" }") == Dict("key" => "value")
93+
# Vertical tab and form feed are NOT valid JSON whitespace (RFC 8259 §2)
94+
@test_throws ArgumentError JSONX.parse("\x0bnull") # vertical tab
95+
@test_throws ArgumentError JSONX.parse("\x0cnull") # form feed
9396
end
9497

9598
@testset "Error Cases" begin
@@ -108,6 +111,9 @@ include("jsonx.jl")
108111
# Test invalid escape sequences
109112
@test_throws ArgumentError JSONX.parse("\"\\x\"")
110113
@test_throws ArgumentError JSONX.parse("\"\\u123\"")
114+
# Leading '+' is not valid JSON (only '-' is a valid leading sign)
115+
@test_throws ArgumentError JSONX.parse("+1")
116+
@test_throws ArgumentError JSONX.parse("+3.14")
111117
end
112118

113119
@testset "JSON Writing" begin

0 commit comments

Comments
 (0)