toJson, jsonTo, json (de)serialization for custom types; remove dependency 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 7cb4ef26ad.
* better approach via mixin
* toJson, jsonTo
* fix test
* address comments
* move to jsonutils
* doc
* cleanups
* also test for js
* also test for vm
This commit is contained in:
parent
733bd76f6b
commit
c7a1a7b8bf
5 changed files with 203 additions and 31 deletions
126
lib/std/jsonutils.nim
Normal file
126
lib/std/jsonutils.nim
Normal file
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue