Skip to content

Commit

Permalink
Allow specifying a value to use when parsing null (#289)
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
ararslan authored Jul 16, 2019
1 parent 1231b52 commit 1383399
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 13 deletions.
36 changes: 23 additions & 13 deletions src/Parser.jl
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ mutable struct StreamingParserState{T <: IO} <: ParserState
end
StreamingParserState(io::IO) = StreamingParserState(io, 0x00, true, PushVector{UInt8}())

struct ParserContext{DictType, IntType, AllowNanInf} end
struct ParserContext{DictType, IntType, AllowNanInf, NullValue} end

"""
Return the byte at the current position of the `ParserState`. If there is no
Expand Down Expand Up @@ -173,7 +173,8 @@ function parse_value(pc::ParserContext, ps::ParserState)
end
end

function parse_jsconstant(::ParserContext{<:Any,<:Any,AllowNanInf}, ps::ParserState) where AllowNanInf
function parse_jsconstant(::ParserContext{<:Any,<:Any,AllowNanInf,NullValue},
ps::ParserState) where {AllowNanInf,NullValue}
c = advance!(ps)
if c == LATIN_T # true
skip!(ps, LATIN_R, LATIN_U, LATIN_E)
Expand All @@ -183,7 +184,7 @@ function parse_jsconstant(::ParserContext{<:Any,<:Any,AllowNanInf}, ps::ParserSt
false
elseif c == LATIN_N # null
skip!(ps, LATIN_U, LATIN_L, LATIN_L)
nothing
NullValue
elseif AllowNanInf && c == LATIN_UPPER_N
skip!(ps, LATIN_A, LATIN_UPPER_N)
NaN
Expand Down Expand Up @@ -427,20 +428,21 @@ function unparameterize_type(T::Type)
end

# Workaround for slow dynamic dispatch for creating objects
const DEFAULT_PARSERCONTEXT = ParserContext{Dict{String, Any}, Int64, false}()
function _get_parsercontext(dicttype, inttype, allownan)
const DEFAULT_PARSERCONTEXT = ParserContext{Dict{String, Any}, Int64, false, nothing}()
function _get_parsercontext(dicttype, inttype, allownan, null)
if dicttype == Dict{String, Any} && inttype == Int64 && !allownan
DEFAULT_PARSERCONTEXT
else
ParserContext{unparameterize_type(dicttype), inttype, allownan}.instance
ParserContext{unparameterize_type(dicttype), inttype, allownan, null}.instance
end
end

"""
parse{T<:Associative}(str::AbstractString;
dicttype::Type{T}=Dict,
inttype::Type{<:Real}=Int64,
allownan::Bool=true)
allownan::Bool=true,
null=nothing)
Parses the given JSON string into corresponding Julia types.
Expand All @@ -449,12 +451,14 @@ Keyword arguments:
• inttype: Real number type to use when parsing JSON numbers that can be parsed
as integers (default: Int64)
• allownan: allow parsing of NaN, Infinity, and -Infinity (default: true)
• null: value to use for parsed JSON `null` values (default: `nothing`)
"""
function parse(str::AbstractString;
dicttype=Dict{String,Any},
inttype::Type{<:Real}=Int64,
allownan::Bool=true)
pc = _get_parsercontext(dicttype, inttype, allownan)
allownan::Bool=true,
null=nothing)
pc = _get_parsercontext(dicttype, inttype, allownan, null)
ps = MemoryParserState(str, 1)
v = parse_value(pc, ps)
chomp_space!(ps)
Expand All @@ -468,7 +472,8 @@ end
parse{T<:Associative}(io::IO;
dicttype::Type{T}=Dict,
inttype::Type{<:Real}=Int64,
allownan=true)
allownan=true,
null=nothing)
Parses JSON from the given IO stream into corresponding Julia types.
Expand All @@ -477,12 +482,14 @@ Keyword arguments:
• inttype: Real number type to use when parsing JSON numbers that can be parsed
as integers (default: Int64)
• allownan: allow parsing of NaN, Infinity, and -Infinity (default: true)
• null: value to use for parsed JSON `null` values (default: `nothing`)
"""
function parse(io::IO;
dicttype=Dict{String,Any},
inttype::Type{<:Real}=Int64,
allownan::Bool=true)
pc = _get_parsercontext(dicttype, inttype, allownan)
allownan::Bool=true,
null=nothing)
pc = _get_parsercontext(dicttype, inttype, allownan, null)
ps = StreamingParserState(io)
parse_value(pc, ps)
end
Expand All @@ -492,6 +499,7 @@ end
dicttype=Dict{String, Any},
inttype::Type{<:Real}=Int64,
allownan::Bool=true,
null=nothing,
use_mmap::Bool=true)
Convenience function to parse JSON from the given file into corresponding Julia types.
Expand All @@ -501,17 +509,19 @@ Keyword arguments:
• inttype: Real number type to use when parsing JSON numbers that can be parsed
as integers (default: Int64)
• allownan: allow parsing of NaN, Infinity, and -Infinity (default: true)
• null: value to use for parsed JSON `null` values (default: `nothing`)
• use_mmap: use mmap when opening the file (default: true)
"""
function parsefile(filename::AbstractString;
dicttype=Dict{String, Any},
inttype::Type{<:Real}=Int64,
null=nothing,
allownan::Bool=true,
use_mmap::Bool=true)
sz = filesize(filename)
open(filename) do io
s = use_mmap ? String(Mmap.mmap(io, Vector{UInt8}, sz)) : read(io, String)
parse(s; dicttype=dicttype, inttype=inttype, allownan=allownan)
parse(s; dicttype=dicttype, inttype=inttype, allownan=allownan, null=null)
end
end

Expand Down
7 changes: 7 additions & 0 deletions test/parser/null.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@testset "Custom null values" begin
s = "{\"x\": null}"
for null in (nothing, missing)
val = JSON.parse(s, null=null)
@test val["x"] === null
end
end
4 changes: 4 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ include("json-samples.jl")
include("parser/nan-inf.jl")
end

@testset "null" begin
include("parser/null.jl")
end

@testset "Miscellaneous" begin
# test for single values
@test JSON.parse("true") == true
Expand Down

0 comments on commit 1383399

Please sign in to comment.