Skip to content

Commit aed2866

Browse files
committed
Add serialization.
1 parent 43daa57 commit aed2866

File tree

4 files changed

+251
-18
lines changed

4 files changed

+251
-18
lines changed

README.md

+53-5
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,42 @@
1-
# JSONy - A loose, direct to object json parser with hooks.
1+
# JSONy - A loose, direct to object json parser and serializer with hooks.
22

33
`nimble install jsony`
44

5+
```nim
6+
@[1, 2, 3].toJson() -> "[1,2,3]"
7+
"[1, 2, 3]".fromJson(seq[int]) -> @[1, 2, 3]
8+
```
9+
510
Real world json is *never what you want*. It might have extra fields that you don't care about. It might have missing fields requiring default values. It might change or grow new fields at any moment. Json might use `camelCase` or `snake_case`. It might use inconsistent naming.
611

7-
With this library you can parse json your way, from the mess you get to the objects you want.
12+
With this library you can use json your way, from the mess you get to the objects you want.
13+
14+
## Fast.
15+
16+
Currently the Nim standard module first parses or serializes json into JsonNodes and then turns the JsonNodes into your objects with the `to()` macro. This is slower and creates unnecessary work for the garbage collector. This library skips the JsonNodes and creates the objects you want directly.
817

9-
## Fast/No garbage.
18+
Another speed up comes from not using `StringStream`. `Stream` has a function dispatch overhead because it has not know if you are using it as a `StringStream` or `FileStream`. Jsony skips the overhead and just directly writes to memory buffers.
19+
20+
### Parse speed.
21+
```
22+
name ............................... min time avg time std dv times
23+
treeform/jsony .................... 10.121 ms 10.809 ms ±1.208 x100
24+
planetis-m/eminim ................. 12.006 ms 12.574 ms ±1.156 x100
25+
nim std/json ...................... 23.282 ms 34.679 ms ±7.512 x100
26+
```
27+
28+
### Serialize speed.
29+
```
30+
name ............................... min time avg time std dv times
31+
treeform/jsony ..................... 4.047 ms 4.172 ms ±0.225 x100
32+
planetis-m/eminim .................. 7.173 ms 7.324 ms ±0.253 x100
33+
disruptek/jason ................... 10.220 ms 11.155 ms ±0.689 x100
34+
nim std/json ...................... 11.526 ms 15.181 ms ±0.857 x100
35+
```
1036

11-
Currently the Nim standard module first parses json into JsonNodes and then turns the JsonNodes into your objects with the `to()` macro. This is slower and creates unnecessary work for the garbage collector. This library skips the JsonNodes and creates the objects you want directly.
37+
Note: If you find a faster nim json parser or serializer let me know!
1238

13-
## Can parse most object types:
39+
## Can parse or serializer most types:
1440

1541
* numbers and strings
1642
* objects and ref objects
@@ -169,3 +195,25 @@ Gives us:
169195
(id: "3", count: 99, filled: 99)
170196
]"""
171197
```
198+
199+
### `proc dumpHook()` Can be used to serializer into custom representation.
200+
201+
Just like reading custom data types you can also write faster data types with `dumpHook`. Using `Fraction` from the above example:
202+
203+
```nim
204+
proc dumpHook(s: var string, v: Fraction) =
205+
## Output fraction type as a string "x/y".
206+
s.add '"'
207+
s.add $v.numerator
208+
s.add '/'
209+
s.add $v.denominator
210+
s.add '"'
211+
212+
var f = Fraction(numerator: 10, denominator: 13)
213+
let s = f.toJson()
214+
```
215+
216+
Gives us:
217+
```
218+
"10/13"
219+
```

src/jsony.nim

+78
Original file line numberDiff line numberDiff line change
@@ -302,3 +302,81 @@ proc fromJson*[T](s: string): T =
302302

303303
var i = 0
304304
parseHook(s, i, result)
305+
306+
proc dumpHook*(s: var string, v: bool) =
307+
if v:
308+
s.add "true"
309+
else:
310+
s.add "false"
311+
312+
proc dumpHook*(s: var string, v: SomeNumber) =
313+
s.add $v
314+
315+
proc dumpHook*(s: var string, v: string) =
316+
s.add '"'
317+
for c in v:
318+
case c:
319+
of '\\': s.add r"\\"
320+
of '\b': s.add r"\b"
321+
of '\f': s.add r"\f"
322+
of '\n': s.add r"\n"
323+
of '\r': s.add r"\r"
324+
of '\t': s.add r"\t"
325+
else:
326+
s.add c
327+
s.add '"'
328+
329+
proc dumpHook*(s: var string, v: char) =
330+
s.add '"'
331+
s.add v
332+
s.add '"'
333+
334+
proc dumpHook*(s: var string, v: object) =
335+
s.add '{'
336+
var i = 0
337+
when compiles(for k, e in v.pairs: discard):
338+
# Tables and table like objects.
339+
for k, e in v.pairs:
340+
if i > 0:
341+
s.add ','
342+
s.dumpHook(k)
343+
s.add ':'
344+
s.dumpHook(e)
345+
inc i
346+
else:
347+
# Normal objects.
348+
for k, e in v.fieldPairs:
349+
if i > 0:
350+
s.add ','
351+
s.dumpHook(k)
352+
s.add ':'
353+
s.dumpHook(e)
354+
inc i
355+
s.add '}'
356+
357+
proc dumpHook*(s: var string, v: ref object) =
358+
if v == nil:
359+
s.add "null"
360+
else:
361+
s.dumpHook(v[])
362+
363+
proc dumpHook*(s: var string, v: tuple) =
364+
s.add '['
365+
var i = 0
366+
for _, e in v.fieldPairs:
367+
if i > 0:
368+
s.add ','
369+
s.dumpHook(e)
370+
inc i
371+
s.add ']'
372+
373+
proc dumpHook*[T](s: var string, v: openarray[T]) =
374+
s.add '['
375+
for i, e in v:
376+
if i != 0:
377+
s.add ','
378+
s.dumpHook(e)
379+
s.add ']'
380+
381+
proc toJson*[T](v: T): string =
382+
dumpHook(result, v)

tests/bench.nim

+40-13
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import benchy, random, json, jsony, eminim, streams, jsutils/jsons
2-
from packedjson import parseJson, toJsonNode
3-
from packedjson/deserialiser import to
1+
import benchy, random, streams
2+
import jsony, jason
3+
import eminim
4+
#import packedjson, packedjson/deserialiser
5+
import json
46

57
type Node = ref object
8+
active: bool
69
kind: string
710
name: string
811
id: int
@@ -13,30 +16,54 @@ var genId: int
1316
proc genTree(depth: int): Node =
1417
result = Node()
1518
result.id = genId
19+
if r.rand(0 .. 1) == 0:
20+
result.active = true
1621
inc genId
1722
result.name = "node" & $result.id
1823
result.kind = "NODE"
1924
if depth > 0:
2025
for i in 0 .. r.rand(0..3):
2126
result.kids.add genTree(depth - 1)
27+
for i in 0 .. r.rand(0..3):
28+
result.kids.add nil
2229

2330
var tree = genTree(10)
2431

25-
var treeStr = pretty(%tree)
32+
var treeStr = tree.toJson()
2633

2734
echo genId, " node tree:"
2835

29-
timeIt "treeform/jsony":
36+
timeIt "treeform/jsony", 100:
3037
keep jsony.fromJson[Node](treeStr)
3138

32-
timeIt "nim std/json":
33-
keep json.parseJson(treeStr).to(Node)
34-
35-
timeIt "araq/packedjson":
36-
keep deserialiser.to(packedjson.parseJson(treeStr).toJsonNode(), Node)
39+
timeIt "nim std/json", 100:
40+
keep json.to(json.parseJson(treeStr), Node)
3741

38-
timeIt "treeform/jsutils/jsons":
39-
keep jsons.fromJson(json.parseJson(treeStr), Node)
42+
# timeIt "araq/packedjson", 100:
43+
# keep deserialiser.to(packedjson.parseJson(treeStr), Node)
4044

41-
timeIt "planetis-m/eminim":
45+
timeIt "planetis-m/eminim", 100:
4246
keep newStringStream(treeStr).jsonTo(Node)
47+
48+
# timeIt "disruptek/jason", 100:
49+
# discard
50+
51+
echo "serialize:"
52+
53+
timeIt "treeform/jsony", 100:
54+
keep tree.toJson()
55+
56+
timeIt "nim std/json", 100:
57+
keep json.`$`(json.`%`(tree))
58+
59+
# timeIt "araq/packedjson", 100:
60+
# keep packedjson.`$`(packedjson.`%`(tree))
61+
62+
timeIt "planetis-m/eminim", 100:
63+
var s = newStringStream()
64+
s.storeJson(tree)
65+
s.setPosition(0)
66+
keep s.data
67+
68+
timeIt "disruptek/jason", 100:
69+
keep tree.jason

tests/test_tojson.nim

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import jsony, strutils, json, tables
2+
3+
proc match[T](what: T) =
4+
doAssert what.toJson() == $(%what)
5+
6+
doAssert 1.uint8.toJson() == "1"
7+
doAssert 1.uint16.toJson() == "1"
8+
doAssert 1.uint32.toJson() == "1"
9+
doAssert 1.uint64.toJson() == "1"
10+
doAssert 1.int8.toJson() == "1"
11+
doAssert 1.int16.toJson() == "1"
12+
doAssert 1.int32.toJson() == "1"
13+
doAssert 1.int64.toJson() == "1"
14+
doAssert 3.14.float32.toJson() == "3.140000104904175"
15+
doAssert 3.14.float64.toJson() == "3.14"
16+
17+
match 1
18+
match 3.14.float32
19+
match 3.14.float64
20+
21+
doAssert [1, 2, 3].toJson() == "[1,2,3]"
22+
doAssert @[1, 2, 3].toJson() == "[1,2,3]"
23+
24+
match [1, 2, 3]
25+
match @[1, 2, 3]
26+
27+
doAssert true.toJson == "true"
28+
doAssert false.toJson == "false"
29+
30+
doAssert 'a'.toJson == "\"a\""
31+
match "hi there"
32+
match "hi\nthere\b\f\n\r\t"
33+
match "как дела"
34+
35+
block:
36+
type
37+
Obj = object
38+
a: int
39+
b: float
40+
c: string
41+
var obj = Obj()
42+
doAssert obj.toJson() == """{"a":0,"b":0.0,"c":""}"""
43+
match obj
44+
45+
block:
46+
type
47+
Obj = ref object
48+
a: int
49+
b: float
50+
c: string
51+
var obj = Obj()
52+
doAssert obj.toJson() == """{"a":0,"b":0.0,"c":""}"""
53+
match obj
54+
55+
var obj2: Obj
56+
doAssert obj2.toJson() == "null"
57+
match obj
58+
59+
var t = (1, 2.2, "hi")
60+
doAssert t.toJson() == """[1,2.2,"hi"]"""
61+
62+
var tb: Table[string, int]
63+
tb["hi"] = 1
64+
tb["bye"] = 2
65+
doAssert tb.toJson() == """{"hi":1,"bye":2}"""
66+
67+
type Fraction = object
68+
numerator: int
69+
denominator: int
70+
71+
proc dumpHook(s: var string, v: Fraction) =
72+
## Output fraction type as a string "x/y".
73+
s.add '"'
74+
s.add $v.numerator
75+
s.add '/'
76+
s.add $v.denominator
77+
s.add '"'
78+
79+
var f = Fraction(numerator: 10, denominator: 13)
80+
doAssert f.toJson() == "\"10/13\""

0 commit comments

Comments
 (0)