-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
toJson
, jsonTo
, json (de)serialization for custom types; remove d…
…ependency on strtabs thanks to a hooking mechanism (#14563) * json custom serialization; application for strtabs * serialize using nesting * make toJson more feature complete * add since * Revert "Improve JSON serialisation of strtabs (#14549)" This reverts commit 7cb4ef2. * better approach via mixin * toJson, jsonTo * fix test * address comments * move to jsonutils * doc * cleanups * also test for js * also test for vm
- Loading branch information
1 parent
733bd76
commit c7a1a7b
Showing
5 changed files
with
203 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
##[ | ||
This module implements a hookable (de)serialization for arbitrary types. | ||
Design goal: avoid importing modules where a custom serialization is needed; | ||
see strtabs.fromJsonHook,toJsonHook for an example. | ||
]## | ||
|
||
import std/[json,tables,strutils] | ||
|
||
#[ | ||
xxx | ||
use toJsonHook,fromJsonHook for Table|OrderedTable | ||
add Options support also using toJsonHook,fromJsonHook and remove `json=>options` dependency | ||
future direction: | ||
add a way to customize serialization, for eg allowing missing | ||
or extra fields in JsonNode, field renaming, and a way to handle cyclic references | ||
using a cache of already visited addresses. | ||
]# | ||
|
||
proc isNamedTuple(T: typedesc): bool {.magic: "TypeTrait".} | ||
proc distinctBase(T: typedesc): typedesc {.magic: "TypeTrait".} | ||
template distinctBase[T](a: T): untyped = distinctBase(type(a))(a) | ||
|
||
proc checkJsonImpl(cond: bool, condStr: string, msg = "") = | ||
if not cond: | ||
# just pick 1 exception type for simplicity; other choices would be: | ||
# JsonError, JsonParser, JsonKindError | ||
raise newException(ValueError, msg) | ||
|
||
template checkJson(cond: untyped, msg = "") = | ||
checkJsonImpl(cond, astToStr(cond), msg) | ||
|
||
proc fromJson*[T](a: var T, b: JsonNode) = | ||
## inplace version of `jsonTo` | ||
#[ | ||
adding "json path" leading to `b` can be added in future work. | ||
]# | ||
checkJson b != nil, $($T, b) | ||
when compiles(fromJsonHook(a, b)): fromJsonHook(a, b) | ||
elif T is bool: a = to(b,T) | ||
elif T is Table | OrderedTable: | ||
a.clear | ||
for k,v in b: | ||
a[k] = jsonTo(v, typeof(a[k])) | ||
elif T is enum: | ||
case b.kind | ||
of JInt: a = T(b.getBiggestInt()) | ||
of JString: a = parseEnum[T](b.getStr()) | ||
else: checkJson false, $($T, " ", b) | ||
elif T is Ordinal: a = T(to(b, int)) | ||
elif T is pointer: a = cast[pointer](to(b, int)) | ||
elif T is distinct: | ||
when nimvm: | ||
# bug, potentially related to https://github.com/nim-lang/Nim/issues/12282 | ||
a = T(jsonTo(b, distinctBase(T))) | ||
else: | ||
a.distinctBase.fromJson(b) | ||
elif T is string|SomeNumber: a = to(b,T) | ||
elif T is JsonNode: a = b | ||
elif T is ref | ptr: | ||
if b.kind == JNull: a = nil | ||
else: | ||
a = T() | ||
fromJson(a[], b) | ||
elif T is array: | ||
checkJson a.len == b.len, $(a.len, b.len, $T) | ||
for i, val in b.getElems: | ||
fromJson(a[i], val) | ||
elif T is seq: | ||
a.setLen b.len | ||
for i, val in b.getElems: | ||
fromJson(a[i], val) | ||
elif T is object | tuple: | ||
const isNamed = T is object or isNamedTuple(T) | ||
when isNamed: | ||
checkJson b.kind == JObject, $(b.kind) # we could customize whether to allow JNull | ||
var num = 0 | ||
for key, val in fieldPairs(a): | ||
num.inc | ||
if b.hasKey key: | ||
fromJson(val, b[key]) | ||
else: | ||
# we could customize to allow this | ||
checkJson false, $($T, key, b) | ||
checkJson b.len == num, $(b.len, num, $T, b) # could customize | ||
else: | ||
checkJson b.kind == JArray, $(b.kind) # we could customize whether to allow JNull | ||
var i = 0 | ||
for val in fields(a): | ||
fromJson(val, b[i]) | ||
i.inc | ||
else: | ||
# checkJson not appropriate here | ||
static: doAssert false, "not yet implemented: " & $T | ||
|
||
proc jsonTo*(b: JsonNode, T: typedesc): T = | ||
## reverse of `toJson` | ||
fromJson(result, b) | ||
|
||
proc toJson*[T](a: T): JsonNode = | ||
## serializes `a` to json; uses `toJsonHook(a: T)` if it's in scope to | ||
## customize serialization, see strtabs.toJsonHook for an example. | ||
when compiles(toJsonHook(a)): result = toJsonHook(a) | ||
elif T is Table | OrderedTable: | ||
result = newJObject() | ||
for k, v in pairs(a): result[k] = toJson(v) | ||
elif T is object | tuple: | ||
const isNamed = T is object or isNamedTuple(T) | ||
when isNamed: | ||
result = newJObject() | ||
for k, v in a.fieldPairs: result[k] = toJson(v) | ||
else: | ||
result = newJArray() | ||
for v in a.fields: result.add toJson(v) | ||
elif T is ref | ptr: | ||
if a == nil: result = newJNull() | ||
else: result = toJson(a[]) | ||
elif T is array | seq: | ||
result = newJArray() | ||
for ai in a: result.add toJson(ai) | ||
elif T is pointer: result = toJson(cast[int](a)) | ||
elif T is distinct: result = toJson(a.distinctBase) | ||
elif T is bool: result = %(a) | ||
elif T is Ordinal: result = %(a.ord) | ||
else: result = %a |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
discard """ | ||
targets: "c cpp js" | ||
""" | ||
|
||
import std/jsonutils | ||
import std/json | ||
|
||
proc testRoundtrip[T](t: T, expected: string) = | ||
let j = t.toJson | ||
doAssert $j == expected, $j | ||
doAssert j.jsonTo(T).toJson == j | ||
|
||
import tables | ||
import strtabs | ||
|
||
template fn() = | ||
block: # toJson, jsonTo | ||
type Foo = distinct float | ||
testRoundtrip('x', """120""") | ||
when not defined(js): | ||
testRoundtrip(cast[pointer](12345)): """12345""" | ||
|
||
# causes workaround in `fromJson` potentially related to | ||
# https://github.com/nim-lang/Nim/issues/12282 | ||
testRoundtrip(Foo(1.5)): """1.5""" | ||
|
||
block: | ||
testRoundtrip({"z": "Z", "y": "Y"}.toOrderedTable): """{"z":"Z","y":"Y"}""" | ||
when not defined(js): # pending https://github.com/nim-lang/Nim/issues/14574 | ||
testRoundtrip({"z": (f1: 'f'), }.toTable): """{"z":{"f1":102}}""" | ||
|
||
block: | ||
testRoundtrip({"name": "John", "city": "Monaco"}.newStringTable): """{"mode":"modeCaseSensitive","table":{"city":"Monaco","name":"John"}}""" | ||
|
||
block: # complex example | ||
let t = {"z": "Z", "y": "Y"}.newStringTable | ||
type A = ref object | ||
a1: string | ||
let a = (1.1, "fo", 'x', @[10,11], [true, false], [t,newStringTable()], [0'i8,3'i8], -4'i16, (foo: 0.5'f32, bar: A(a1: "abc"), bar2: A.default)) | ||
testRoundtrip(a): | ||
"""[1.1,"fo",120,[10,11],[true,false],[{"mode":"modeCaseSensitive","table":{"y":"Y","z":"Z"}},{"mode":"modeCaseSensitive","table":{}}],[0,3],-4,{"foo":0.5,"bar":{"a1":"abc"},"bar2":null}]""" | ||
|
||
static: fn() | ||
fn() |