Skip to content

Commit 313dff5

Browse files
Partial decoding while traversing
1 parent 3ad61bf commit 313dff5

File tree

3 files changed

+317
-254
lines changed

3 files changed

+317
-254
lines changed

README.md

Lines changed: 73 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -40,104 +40,103 @@ Primes are:
4040
4 7
4141
```
4242

43-
### Traversing (decoding JSON without creating the result on Lua side)
43+
### Traversing (dry run decoding JSON without creating the result on Lua side) and partial decoding of JSON
4444

45-
Function `json.traverse(s, callback, pos)` traverses JSON and invokes user-supplied callback function
45+
Traverse is useful to reduce memory usage: no memory-consuming objects are being created in Lua while traversing.
46+
Function `json.traverse(s, callback, pos)` traverses JSON,
47+
and invokes user-supplied callback function for each item found inside JSON.
48+
49+
Callback function arguments:
4650
```
47-
callback function arguments: (path, json_type, value, pos)
51+
path, json_type, value, pos, pos_last
4852
path is array of nested JSON identifiers, this array is empty for root JSON element
4953
json_type is one of "null"/"boolean"/"number"/"string"/"array"/"object"
50-
value is defined when json_type is "boolean"/"number"/"string", otherwise value == nil
51-
pos is 1-based index of first character of current JSON element inside JSON string
52-
```
53-
```lua
54-
json.traverse([[ 42 ]], callback)
55-
-- will invoke callback function 1 time:
56-
callback( {}, "number", 42, 2 )
54+
value is defined when json_type is "null"/"boolean"/"number"/"string", value == nil for "object"/"array"
55+
pos is 1-based index of first character of current JSON element
56+
pos_last is 1-based index of last character of current JSON element (defined only when "value" ~= nil)
5757
```
58-
```lua
59-
json.traverse([[ {"a":true, "b":null, "c":["one","two"], "d":{ "e":{}, "f":[] } } ]], callback)
60-
-- will invoke callback function 9 times:
61-
callback( {}, "object", nil, 2 )
62-
callback( {"a"}, "boolean", true, 7 )
63-
callback( {"b"}, "null", nil, 17 )
64-
callback( {"c"}, "array", nil, 27 )
65-
callback( {"c", 1}, "string", "one", 28 )
66-
callback( {"c", 2}, "string", "two", 34 )
67-
callback( {"d"}, "object", nil, 46 )
68-
callback( {"d", "e"}, "object", nil, 52 )
69-
callback( {"d", "f"}, "array", nil, 60 )
70-
```
71-
58+
By default, `value` is `nil` for JSON arrays/objects.
59+
Nevertheless, you can get any array/object decoded (instead of get traversed) while traversing JSON.
60+
In order to do that, callback function must return true when invoked for that element (when `value == nil`).
61+
This array/object decoded as Lua value will be sent to you on next invocation of callback function (`value ~= nil`).
7262

73-
### Reading JSON chunk-by-chunk
63+
Traverse example:
7464

75-
Both decoder functions `json.decode()` and `json.traverse()` can accept JSON as a "loader function" instead of a string.
76-
This function will be called repeatedly to return next parts (substrings) of JSON string.
77-
An empty string, nil, or no value returned from "loader function" means the end of JSON string.
78-
This may be useful for low-memory devices or for traversing huge JSON files.
65+
```lua
66+
json.traverse([[ {"a":true, "b":null, "c":["one","two"], "d":{ "e":{}, "f":[] }, "g":["ten",20,-33.3] } ]], callback)
67+
-- will invoke callback function 13 times (if callback returns true for array "c" and object "e"):
68+
-- path json_type value pos pos_last
69+
-- ---------- --------- -------------- --- --------
70+
callback( {}, "object", nil, 2, nil )
71+
callback( {"a"}, "boolean", true, 7, 10 )
72+
callback( {"b"}, "null", json.null, 17, 20 ) -- special Lua value for JSON null
73+
callback( {"c"}, "array", nil, 27, nil ) -- this callback returned true (user wants to decode this array)
74+
callback( {"c"}, "array", {"one", "two"}, 27, 39 ) -- the next invocation brings the result of decoding (value ~= nil)
75+
callback( {"d"}, "object", nil, 46, nil )
76+
callback( {"d", "e"}, "object", nil, 52, nil ) -- this callback returned true (user wants to decode this object)
77+
callback( {"d", "e"}, "object", json.empty, 52, 53 ) -- the next invocation brings the result of decoding (special Lua value for empty JSON object)
78+
callback( {"d", "f"}, "array", nil, 60, nil )
79+
callback( {"g"}, "array", nil, 70, nil )
80+
callback( {"g", 1}, "string", "ten", 71, 75 )
81+
callback( {"g", 2}, "number", 20, 77, 78 )
82+
callback( {"g", 3}, "number", -33.3, 80, 84 )
83+
```
7984

85+
Example of callback function to get `c` and `e` elements been decoded (instead of traversed):
8086

81-
### Partial decoding of arbitrary element inside JSON
87+
```lua
88+
local result_c, result_e -- these variables will hold Lua objects for JSON elements "c" and "e"
89+
90+
local function callback(path, json_type, value, pos, pos_last)
91+
-- print(table.concat(path, '/'), json_type, value, pos, pos_last)
92+
local elem_name = path[#path] -- last identifier in element's path
93+
if elem_name == "c" then
94+
result_c = value
95+
elseif elem_name == "e" then
96+
result_e = value
97+
end
98+
return elem_name == "c" or elem_name == "e" -- we want elements "c" and "e" to be decoded instead of be traversed
99+
end
82100

83-
Instead of decoding whole JSON, you can decode its arbitrary element (e.g, array or object) by specifying the position where this element starts.
84-
In order to do that, at first you have to traverse JSON to get all positions you need.
101+
json.traverse(JSON_string, callback)
102+
```
85103

104+
### Reading JSON from file without preloading whole JSON as (huge) Lua string
86105

87-
### Partial decoding of JSON with reading JSON from file
106+
Both functions `json.decode()` and `json.traverse()` can accept JSON as a "loader function" instead of a "whole JSON string".
107+
This function will be called repeatedly to return next parts (substrings) of JSON.
108+
An empty string, nil, or no value returned from "loader function" means the end of JSON.
109+
This may be useful for low-memory devices or for traversing huge JSON files.
88110

89111
```lua
90-
-- This is content of "data.txt" file:
91-
-- {"aa":["qq",{"k1":23,"gg":"YAY","Fermat_primes":[3, 5, 17, 257, 65537],"bb":0}]}
92-
93-
-- We want to extract (as Lua array) only "Fermat_primes" from this JSON
94-
-- without loading whole JSON to Lua.
95-
96-
local json = require('json')
97-
98112
-- Open file
99-
local file = io.open('data.txt', 'r')
113+
local file = assert(io.open('large_json.txt', 'r'))
100114

101-
-- Define loader function which will read the file in 64-byte chunks
115+
-- Define loader function for reading the file in 4KByte chunks
102116
local function my_json_loader()
103-
return file:read(64)
117+
return file:read(4*1024) -- 64 Byte chunks are recommended for RAM-restricted devices
104118
end
105119

106-
local position -- We need to know the position of Fermat primes array inside this JSON
107-
108-
-- Prepare callback function for traverse
109-
local function my_callback (path, json_type, value, pos)
110-
if #path == 3
111-
and path[1] == "aa"
112-
and path[2] == 2
113-
and path[3] == "Fermat_primes"
114-
and json_type == "array"
115-
then
116-
position = pos
120+
if you_want_to_traverse_JSON_or_to_decode_file_partially then
121+
122+
-- Prepare callback function
123+
local function my_callback (path, json_type, value, pos, last_pos)
124+
-- Do whatever you need here
125+
-- (see examples on using json.traverse)
117126
end
118-
end
119127

120-
-- Step #1: Traverse to get the position
121-
file:seek("set", 0)
122-
json.traverse(my_json_loader, my_callback)
128+
-- Do traverse
129+
-- Set initial position as 3-rd argument (default 1) if JSON is stored not from the beginning of your file
130+
json.traverse(my_json_loader, my_callback)
123131

124-
-- Step #2: Decode only Fermat_primes array
125-
file:seek("set", position - 1)
126-
local FP = json.decode(my_json_loader)
132+
elseif you_want_to_decode_the_whole_file then
127133

128-
print('Fermat_primes:'); for k, v in ipairs(FP) do print(k, v) end
134+
-- Do decode
135+
-- Set initial position as 2-rd argument (default 1) if JSON is stored not from the beginning of your file
136+
result = json.decode(my_json_loader)
129137

138+
end
139+
130140
-- Close file
131141
file:close()
132142
```
133-
134-
Output:
135-
136-
```
137-
Fermat_primes:
138-
1 3
139-
2 5
140-
3 17
141-
4 257
142-
5 65537
143-
```

0 commit comments

Comments
 (0)