Skip to content

Commit 1383399

Browse files
authored
Allow specifying a value to use when parsing null (#289)
Currently both `nothing` and `missing` are written to JSON files as `null`, but parsing such files results in `nothing`. Now the user can pass a `null=` keyword argument to `parse` and `parsefile` to specify the Julia value that results from parsing JSON's `null`.
1 parent 1231b52 commit 1383399

File tree

3 files changed

+34
-13
lines changed

3 files changed

+34
-13
lines changed

src/Parser.jl

+23-13
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ mutable struct StreamingParserState{T <: IO} <: ParserState
3737
end
3838
StreamingParserState(io::IO) = StreamingParserState(io, 0x00, true, PushVector{UInt8}())
3939

40-
struct ParserContext{DictType, IntType, AllowNanInf} end
40+
struct ParserContext{DictType, IntType, AllowNanInf, NullValue} end
4141

4242
"""
4343
Return the byte at the current position of the `ParserState`. If there is no
@@ -173,7 +173,8 @@ function parse_value(pc::ParserContext, ps::ParserState)
173173
end
174174
end
175175

176-
function parse_jsconstant(::ParserContext{<:Any,<:Any,AllowNanInf}, ps::ParserState) where AllowNanInf
176+
function parse_jsconstant(::ParserContext{<:Any,<:Any,AllowNanInf,NullValue},
177+
ps::ParserState) where {AllowNanInf,NullValue}
177178
c = advance!(ps)
178179
if c == LATIN_T # true
179180
skip!(ps, LATIN_R, LATIN_U, LATIN_E)
@@ -183,7 +184,7 @@ function parse_jsconstant(::ParserContext{<:Any,<:Any,AllowNanInf}, ps::ParserSt
183184
false
184185
elseif c == LATIN_N # null
185186
skip!(ps, LATIN_U, LATIN_L, LATIN_L)
186-
nothing
187+
NullValue
187188
elseif AllowNanInf && c == LATIN_UPPER_N
188189
skip!(ps, LATIN_A, LATIN_UPPER_N)
189190
NaN
@@ -427,20 +428,21 @@ function unparameterize_type(T::Type)
427428
end
428429

429430
# Workaround for slow dynamic dispatch for creating objects
430-
const DEFAULT_PARSERCONTEXT = ParserContext{Dict{String, Any}, Int64, false}()
431-
function _get_parsercontext(dicttype, inttype, allownan)
431+
const DEFAULT_PARSERCONTEXT = ParserContext{Dict{String, Any}, Int64, false, nothing}()
432+
function _get_parsercontext(dicttype, inttype, allownan, null)
432433
if dicttype == Dict{String, Any} && inttype == Int64 && !allownan
433434
DEFAULT_PARSERCONTEXT
434435
else
435-
ParserContext{unparameterize_type(dicttype), inttype, allownan}.instance
436+
ParserContext{unparameterize_type(dicttype), inttype, allownan, null}.instance
436437
end
437438
end
438439

439440
"""
440441
parse{T<:Associative}(str::AbstractString;
441442
dicttype::Type{T}=Dict,
442443
inttype::Type{<:Real}=Int64,
443-
allownan::Bool=true)
444+
allownan::Bool=true,
445+
null=nothing)
444446
445447
Parses the given JSON string into corresponding Julia types.
446448
@@ -449,12 +451,14 @@ Keyword arguments:
449451
• inttype: Real number type to use when parsing JSON numbers that can be parsed
450452
as integers (default: Int64)
451453
• allownan: allow parsing of NaN, Infinity, and -Infinity (default: true)
454+
• null: value to use for parsed JSON `null` values (default: `nothing`)
452455
"""
453456
function parse(str::AbstractString;
454457
dicttype=Dict{String,Any},
455458
inttype::Type{<:Real}=Int64,
456-
allownan::Bool=true)
457-
pc = _get_parsercontext(dicttype, inttype, allownan)
459+
allownan::Bool=true,
460+
null=nothing)
461+
pc = _get_parsercontext(dicttype, inttype, allownan, null)
458462
ps = MemoryParserState(str, 1)
459463
v = parse_value(pc, ps)
460464
chomp_space!(ps)
@@ -468,7 +472,8 @@ end
468472
parse{T<:Associative}(io::IO;
469473
dicttype::Type{T}=Dict,
470474
inttype::Type{<:Real}=Int64,
471-
allownan=true)
475+
allownan=true,
476+
null=nothing)
472477
473478
Parses JSON from the given IO stream into corresponding Julia types.
474479
@@ -477,12 +482,14 @@ Keyword arguments:
477482
• inttype: Real number type to use when parsing JSON numbers that can be parsed
478483
as integers (default: Int64)
479484
• allownan: allow parsing of NaN, Infinity, and -Infinity (default: true)
485+
• null: value to use for parsed JSON `null` values (default: `nothing`)
480486
"""
481487
function parse(io::IO;
482488
dicttype=Dict{String,Any},
483489
inttype::Type{<:Real}=Int64,
484-
allownan::Bool=true)
485-
pc = _get_parsercontext(dicttype, inttype, allownan)
490+
allownan::Bool=true,
491+
null=nothing)
492+
pc = _get_parsercontext(dicttype, inttype, allownan, null)
486493
ps = StreamingParserState(io)
487494
parse_value(pc, ps)
488495
end
@@ -492,6 +499,7 @@ end
492499
dicttype=Dict{String, Any},
493500
inttype::Type{<:Real}=Int64,
494501
allownan::Bool=true,
502+
null=nothing,
495503
use_mmap::Bool=true)
496504
497505
Convenience function to parse JSON from the given file into corresponding Julia types.
@@ -501,17 +509,19 @@ Keyword arguments:
501509
• inttype: Real number type to use when parsing JSON numbers that can be parsed
502510
as integers (default: Int64)
503511
• allownan: allow parsing of NaN, Infinity, and -Infinity (default: true)
512+
• null: value to use for parsed JSON `null` values (default: `nothing`)
504513
• use_mmap: use mmap when opening the file (default: true)
505514
"""
506515
function parsefile(filename::AbstractString;
507516
dicttype=Dict{String, Any},
508517
inttype::Type{<:Real}=Int64,
518+
null=nothing,
509519
allownan::Bool=true,
510520
use_mmap::Bool=true)
511521
sz = filesize(filename)
512522
open(filename) do io
513523
s = use_mmap ? String(Mmap.mmap(io, Vector{UInt8}, sz)) : read(io, String)
514-
parse(s; dicttype=dicttype, inttype=inttype, allownan=allownan)
524+
parse(s; dicttype=dicttype, inttype=inttype, allownan=allownan, null=null)
515525
end
516526
end
517527

test/parser/null.jl

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@testset "Custom null values" begin
2+
s = "{\"x\": null}"
3+
for null in (nothing, missing)
4+
val = JSON.parse(s, null=null)
5+
@test val["x"] === null
6+
end
7+
end

test/runtests.jl

+4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ include("json-samples.jl")
2929
include("parser/nan-inf.jl")
3030
end
3131

32+
@testset "null" begin
33+
include("parser/null.jl")
34+
end
35+
3236
@testset "Miscellaneous" begin
3337
# test for single values
3438
@test JSON.parse("true") == true

0 commit comments

Comments
 (0)