Skip to content

Commit 2ea3c62

Browse files
authored
Merge pull request #1200 from JuliaLang/jsonx
Switch from JSON.jl to JSONX
2 parents 1e1b88f + 08aecd6 commit 2ea3c62

File tree

17 files changed

+767
-40
lines changed

17 files changed

+767
-40
lines changed

Project.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
77
Conda = "8f4d0f93-b110-5947-807f-2305c1781a2d"
88
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
99
InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
10-
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
1110
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
1211
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
1312
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
@@ -31,7 +30,6 @@ Base64 = "1"
3130
Conda = "1"
3231
Dates = "1"
3332
InteractiveUtils = "1"
34-
JSON = "0.18,0.19,0.20,0.21,1"
3533
Logging = "1"
3634
Markdown = "1"
3735
Pkg = "1"

deps/jsonx.jl

Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
1+
# Copied from:
2+
# https://github.com/JuliaIO/JSON.jl/tree/52d8b907fd26c02a13f6d6f807524e71859cc5bb/vendor
3+
4+
5+
module JSONX
6+
7+
"""
8+
JSONX.parse(json_str::String)
9+
JSONX.parse(bytes::AbstractVector{UInt8})
10+
11+
Parse a JSON string or byte array and return a Julia value.
12+
Returns one of: Dict{String, Any}, Vector{Any}, String, Int64, Float64, Bool, or Nothing.
13+
Numbers without decimal points or exponents are parsed as Int64, falling back to Float64 on overflow.
14+
"""
15+
function parse(json_str::String)
16+
pos = 1
17+
len = ncodeunits(json_str)
18+
pos = skip_whitespace(json_str, pos, len)
19+
pos > len && throw(ArgumentError("Empty or whitespace-only JSON"))
20+
result, new_pos = parse_value(json_str, pos, len)
21+
new_pos = skip_whitespace(json_str, new_pos, len)
22+
new_pos <= len && throw(ArgumentError("Trailing content after JSON"))
23+
return result
24+
end
25+
26+
parse(bytes::AbstractVector{UInt8}) = parse(String(bytes))
27+
28+
"""
29+
JSONX.parsefile(filename::String)
30+
31+
Parse a JSON file and return a Julia value.
32+
"""
33+
parsefile(filename::String) = parse(read(filename, String))
34+
35+
"""
36+
JSONX.json(value)
37+
38+
Convert a Julia value to a JSON string.
39+
Supports: AbstractDict, NamedTuple, AbstractVector, AbstractSet, Tuple, AbstractString, Number, Bool, Nothing, Missing.
40+
"""
41+
function json(value)
42+
io = IOBuffer()
43+
write_json(io, value)
44+
return String(take!(io))
45+
end
46+
47+
# Helper function to skip whitespace
48+
function skip_whitespace(str::String, pos::Int, len::Int)
49+
while pos <= len && isspace(Char(codeunit(str, pos)))
50+
pos += 1
51+
end
52+
return pos
53+
end
54+
55+
# Unicode handling functions (adapted from JSON.jl)
56+
utf16_is_surrogate(c::UInt16) = (c & 0xf800) == 0xd800
57+
utf16_get_supplementary(lead::UInt16, trail::UInt16) = Char(UInt32(lead-0xd7f7)<<10 + trail)
58+
59+
function reverseescapechar(b)
60+
b == UInt8('"') && return UInt8('"')
61+
b == UInt8('\\') && return UInt8('\\')
62+
b == UInt8('/') && return UInt8('/')
63+
b == UInt8('b') && return UInt8('\b')
64+
b == UInt8('f') && return UInt8('\f')
65+
b == UInt8('n') && return UInt8('\n')
66+
b == UInt8('r') && return UInt8('\r')
67+
b == UInt8('t') && return UInt8('\t')
68+
return 0x00
69+
end
70+
71+
function unescape_string(str::String, start_pos::Int, end_pos::Int)
72+
# Unescape the string (we know it needs unescaping when this is called)
73+
io = IOBuffer()
74+
pos = start_pos
75+
while pos < end_pos
76+
c = codeunit(str, pos)
77+
if c == UInt8('\\')
78+
pos += 1
79+
pos >= end_pos && throw(ArgumentError("Unexpected end of input in string"))
80+
esc_c = codeunit(str, pos)
81+
if esc_c == UInt8('u')
82+
# Unicode escape sequence
83+
pos + 4 >= end_pos && throw(ArgumentError("Incomplete Unicode escape"))
84+
# Parse 4 hex digits
85+
c = UInt16(0)
86+
for offset in 1:4
87+
bb = codeunit(str, pos + offset)
88+
nv = if UInt8('0') <= bb <= UInt8('9')
89+
bb - UInt8('0')
90+
elseif UInt8('A') <= bb <= UInt8('F')
91+
bb - (UInt8('A') - 10)
92+
elseif UInt8('a') <= bb <= UInt8('f')
93+
bb - (UInt8('a') - 10)
94+
else
95+
throw(ArgumentError("Invalid Unicode escape"))
96+
end
97+
c = (c << 4) + UInt16(nv)
98+
end
99+
pos += 4
100+
101+
if utf16_is_surrogate(c)
102+
# Check for surrogate pair
103+
if pos + 6 < end_pos && codeunit(str, pos + 1) == UInt8('\\') && codeunit(str, pos + 2) == UInt8('u')
104+
# Parse next 4 hex digits
105+
c2 = UInt16(0)
106+
for offset in 3:6
107+
bb = codeunit(str, pos + offset)
108+
nv = if UInt8('0') <= bb <= UInt8('9')
109+
bb - UInt8('0')
110+
elseif UInt8('A') <= bb <= UInt8('F')
111+
bb - (UInt8('A') - 10)
112+
elseif UInt8('a') <= bb <= UInt8('f')
113+
bb - (UInt8('a') - 10)
114+
else
115+
throw(ArgumentError("Invalid Unicode escape"))
116+
end
117+
c2 = (c2 << 4) + UInt16(nv)
118+
end
119+
if utf16_is_surrogate(c2)
120+
# Valid surrogate pair
121+
ch = utf16_get_supplementary(c, c2)
122+
print(io, ch)
123+
pos += 6
124+
else
125+
# Invalid trailing surrogate, treat lead as lone
126+
ch = Char(c)
127+
print(io, ch)
128+
end
129+
else
130+
# Lone surrogate - this is valid, just emit the character
131+
ch = Char(c)
132+
print(io, ch)
133+
end
134+
else
135+
# Non-surrogate Unicode
136+
ch = Char(c)
137+
print(io, ch)
138+
end
139+
else
140+
# Simple escape sequence
141+
b = reverseescapechar(esc_c)
142+
b == 0x00 && throw(ArgumentError("Invalid escape sequence \\$(Char(esc_c))"))
143+
print(io, Char(b))
144+
end
145+
else
146+
print(io, Char(c))
147+
end
148+
pos += 1
149+
end
150+
return String(take!(io))
151+
end
152+
153+
# Parse-related functionality
154+
function parse_value(str::String, pos::Int, len::Int)
155+
pos > len && throw(ArgumentError("Unexpected end of input"))
156+
c = codeunit(str, pos)
157+
if c == UInt8('{')
158+
return parse_object(str, pos, len)
159+
elseif c == UInt8('[')
160+
return parse_array(str, pos, len)
161+
elseif c == UInt8('"')
162+
return parse_string(str, pos, len)
163+
elseif c == UInt8('n') && pos + 3 <= len && codeunit(str, pos + 1) == UInt8('u') && codeunit(str, pos + 2) == UInt8('l') && codeunit(str, pos + 3) == UInt8('l')
164+
return nothing, pos + 4
165+
elseif c == UInt8('t') && pos + 3 <= len && codeunit(str, pos + 1) == UInt8('r') && codeunit(str, pos + 2) == UInt8('u') && codeunit(str, pos + 3) == UInt8('e')
166+
return true, pos + 4
167+
elseif c == UInt8('f') && pos + 4 <= len && codeunit(str, pos + 1) == UInt8('a') && codeunit(str, pos + 2) == UInt8('l') && codeunit(str, pos + 3) == UInt8('s') && codeunit(str, pos + 4) == UInt8('e')
168+
return false, pos + 5
169+
elseif c == UInt8('-') || (UInt8('0') <= c <= UInt8('9'))
170+
return parse_number(str, pos, len)
171+
else
172+
throw(ArgumentError("Invalid JSON value starting at position $pos"))
173+
end
174+
end
175+
176+
function parse_object(str::String, pos::Int, len::Int)
177+
codeunit(str, pos) != UInt8('{') && throw(ArgumentError("Expected '{' at position $pos"))
178+
pos += 1
179+
result = Dict{String, Any}()
180+
pos = skip_whitespace(str, pos, len)
181+
pos > len && throw(ArgumentError("Unexpected end of input in object"))
182+
codeunit(str, pos) == UInt8('}') && return result, pos + 1
183+
while true
184+
codeunit(str, pos) != UInt8('"') && throw(ArgumentError("Expected '\"' at position $pos"))
185+
key, pos = parse_string(str, pos, len)
186+
pos = skip_whitespace(str, pos, len)
187+
(pos > len || codeunit(str, pos) != UInt8(':')) && throw(ArgumentError("Expected ':' at position $pos"))
188+
pos += 1
189+
pos = skip_whitespace(str, pos, len)
190+
pos > len && throw(ArgumentError("Unexpected end of input in object"))
191+
value, pos = parse_value(str, pos, len)
192+
result[key] = value
193+
pos = skip_whitespace(str, pos, len)
194+
pos > len && throw(ArgumentError("Unexpected end of input in object"))
195+
if codeunit(str, pos) == UInt8('}')
196+
return result, pos + 1
197+
elseif codeunit(str, pos) == UInt8(',')
198+
pos += 1
199+
pos = skip_whitespace(str, pos, len)
200+
else
201+
throw(ArgumentError("Expected ',' or '}' at position $pos"))
202+
end
203+
end
204+
end
205+
206+
function parse_array(str::String, pos::Int, len::Int)
207+
codeunit(str, pos) != UInt8('[') && throw(ArgumentError("Expected '[' at position $pos"))
208+
pos += 1
209+
result = []
210+
pos = skip_whitespace(str, pos, len)
211+
pos > len && throw(ArgumentError("Unexpected end of input in array"))
212+
codeunit(str, pos) == UInt8(']') && return result, pos + 1
213+
while true
214+
value, pos = parse_value(str, pos, len)
215+
push!(result, value)
216+
pos = skip_whitespace(str, pos, len)
217+
pos > len && throw(ArgumentError("Unexpected end of input in array"))
218+
if codeunit(str, pos) == UInt8(']')
219+
return result, pos + 1
220+
elseif codeunit(str, pos) == UInt8(',')
221+
pos += 1
222+
pos = skip_whitespace(str, pos, len)
223+
pos <= len && codeunit(str, pos) == UInt8(']') && throw(ArgumentError("Trailing comma not allowed in JSON"))
224+
else
225+
throw(ArgumentError("Expected ',' or ']' at position $pos"))
226+
end
227+
end
228+
end
229+
230+
function parse_string(str::String, pos::Int, len::Int)
231+
codeunit(str, pos) != UInt8('"') && throw(ArgumentError("Expected '\"' at position $pos"))
232+
pos += 1
233+
start_pos = pos
234+
needs_unescape = false
235+
while pos <= len
236+
c = codeunit(str, pos)
237+
if c == UInt8('"')
238+
return needs_unescape ? unescape_string(str, start_pos, pos) : GC.@preserve(str, unsafe_string(pointer(str, start_pos), pos - start_pos)), pos + 1
239+
elseif c == UInt8('\\')
240+
needs_unescape = true
241+
pos += 1
242+
pos > len && throw(ArgumentError("Unexpected end of input in string"))
243+
esc_c = codeunit(str, pos)
244+
if esc_c == UInt8('"') || esc_c == UInt8('\\') || esc_c == UInt8('/') || esc_c == UInt8('b') || esc_c == UInt8('f') || esc_c == UInt8('n') || esc_c == UInt8('r') || esc_c == UInt8('t') || esc_c == UInt8('u')
245+
# Valid escape sequence, continue
246+
else
247+
throw(ArgumentError("Invalid escape sequence \\$(Char(esc_c))"))
248+
end
249+
elseif Int(c) < 0x20
250+
throw(ArgumentError("Control character in string"))
251+
end
252+
pos += 1
253+
end
254+
throw(ArgumentError("Unterminated string"))
255+
end
256+
257+
function parse_number(str::String, pos::Int, len::Int)
258+
start_pos = pos
259+
has_decimal_or_exp = false
260+
while pos <= len
261+
c = codeunit(str, pos)
262+
if c == UInt8('-') || (UInt8('0') <= c <= UInt8('9')) || c == UInt8('+')
263+
pos += 1
264+
elseif c == UInt8('.') || c == UInt8('e') || c == UInt8('E')
265+
has_decimal_or_exp = true
266+
pos += 1
267+
else
268+
break
269+
end
270+
end
271+
num_str = @view str[start_pos:pos-1]
272+
273+
# Try parsing as Int64 if no decimal point or exponent
274+
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
280+
end
281+
282+
# Parse as Float64
283+
try
284+
return Base.parse(Float64, num_str), pos
285+
catch
286+
throw(ArgumentError("Invalid number format"))
287+
end
288+
end
289+
290+
# JSON writing functionality
291+
292+
struct JSONText
293+
text::String
294+
end
295+
296+
function write_json(io::IO, value)
297+
if value === nothing || value === missing
298+
print(io, "null")
299+
elseif value isa Bool
300+
print(io, value ? "true" : "false")
301+
elseif value isa Number
302+
value isa Complex && throw(ArgumentError("Cannot serialize Complex numbers to JSON"))
303+
print(io, value)
304+
elseif value isa JSONText
305+
print(io, value.text)
306+
elseif value isa AbstractString
307+
write_string(io, value)
308+
elseif value isa AbstractVector || value isa AbstractSet || value isa Tuple
309+
write_array(io, value)
310+
elseif value isa AbstractDict || value isa NamedTuple
311+
write_object(io, value)
312+
elseif value isa Symbol || value isa Enum
313+
write_string(io, string(value))
314+
else
315+
throw(ArgumentError("Cannot serialize $(typeof(value)) to JSON"))
316+
end
317+
end
318+
319+
function write_string(io::IO, str::AbstractString)
320+
print(io, '"')
321+
for c in str
322+
if c == '"'
323+
print(io, "\\\"")
324+
elseif c == '\\'
325+
print(io, "\\\\")
326+
elseif c == '\b'
327+
print(io, "\\b")
328+
elseif c == '\f'
329+
print(io, "\\f")
330+
elseif c == '\n'
331+
print(io, "\\n")
332+
elseif c == '\r'
333+
print(io, "\\r")
334+
elseif c == '\t'
335+
print(io, "\\t")
336+
elseif Int(c) < 0x20
337+
print(io, "\\u", string(Int(c), base=16, pad=4))
338+
else
339+
print(io, c)
340+
end
341+
end
342+
print(io, '"')
343+
end
344+
345+
function write_array(io::IO, arr::Union{AbstractVector, AbstractSet, Tuple})
346+
print(io, '[')
347+
for (i, item) in enumerate(arr)
348+
i > 1 && print(io, ',')
349+
write_json(io, item)
350+
end
351+
print(io, ']')
352+
end
353+
354+
function write_object(io::IO, dict::Union{AbstractDict, NamedTuple})
355+
print(io, '{')
356+
first = true
357+
for (key, value) in pairs(dict)
358+
!first && print(io, ',')
359+
first = false
360+
write_string(io, string(key))
361+
print(io, ':')
362+
write_json(io, value)
363+
end
364+
print(io, '}')
365+
end
366+
367+
end # module

0 commit comments

Comments
 (0)