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
|
|
@ -108,6 +108,8 @@
|
||||||
users from the use of weak and insecure ciphers while still provides
|
users from the use of weak and insecure ciphers while still provides
|
||||||
adequate compatiblity with the majority of the Internet.
|
adequate compatiblity with the majority of the Internet.
|
||||||
|
|
||||||
|
- new module `std/jsonutils` with hookable `jsonTo,toJson,fromJson` for json serialization/deserialization of custom types.
|
||||||
|
|
||||||
## Language changes
|
## Language changes
|
||||||
- In the newruntime it is now allowed to assign discriminator field without restrictions as long as case object doesn't have custom destructor. Discriminator value doesn't have to be a constant either. If you have custom destructor for case object and you do want to freely assign discriminator fields, it is recommended to refactor object into 2 objects like this:
|
- In the newruntime it is now allowed to assign discriminator field without restrictions as long as case object doesn't have custom destructor. Discriminator value doesn't have to be a constant either. If you have custom destructor for case object and you do want to freely assign discriminator fields, it is recommended to refactor object into 2 objects like this:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,9 @@
|
||||||
## var j2 = %* {"name": "Isaac", "books": ["Robot Dreams"]}
|
## var j2 = %* {"name": "Isaac", "books": ["Robot Dreams"]}
|
||||||
## j2["details"] = %* {"age":35, "pi":3.1415}
|
## j2["details"] = %* {"age":35, "pi":3.1415}
|
||||||
## echo j2
|
## echo j2
|
||||||
|
##
|
||||||
|
## See also: std/jsonutils for hookable json serialization/deserialization
|
||||||
|
## of arbitrary types.
|
||||||
|
|
||||||
runnableExamples:
|
runnableExamples:
|
||||||
## Note: for JObject, key ordering is preserved, unlike in some languages,
|
## Note: for JObject, key ordering is preserved, unlike in some languages,
|
||||||
|
|
@ -149,8 +152,10 @@ runnableExamples:
|
||||||
doAssert $(%* Foo()) == """{"a1":0,"a2":0,"a0":0,"a3":0,"a4":0}"""
|
doAssert $(%* Foo()) == """{"a1":0,"a2":0,"a0":0,"a3":0,"a4":0}"""
|
||||||
|
|
||||||
import
|
import
|
||||||
hashes, tables, strtabs, strutils, lexbase, streams, macros, parsejson,
|
hashes, tables, strutils, lexbase, streams, macros, parsejson
|
||||||
options
|
|
||||||
|
import options # xxx remove this dependency using same approach as https://github.com/nim-lang/Nim/pull/14563
|
||||||
|
import std/private/since
|
||||||
|
|
||||||
export
|
export
|
||||||
tables.`$`
|
tables.`$`
|
||||||
|
|
@ -353,14 +358,6 @@ proc `[]=`*(obj: JsonNode, key: string, val: JsonNode) {.inline.} =
|
||||||
assert(obj.kind == JObject)
|
assert(obj.kind == JObject)
|
||||||
obj.fields[key] = val
|
obj.fields[key] = val
|
||||||
|
|
||||||
proc `%`*(table: StringTableRef): JsonNode =
|
|
||||||
## Generic constructor for JSON data. Creates a new ``JObject JsonNode``.
|
|
||||||
result = newJObject()
|
|
||||||
result["mode"] = %($table.mode)
|
|
||||||
var data = newJObject()
|
|
||||||
for k, v in table: data[k] = %v
|
|
||||||
result["data"] = data
|
|
||||||
|
|
||||||
proc `%`*[T: object](o: T): JsonNode =
|
proc `%`*[T: object](o: T): JsonNode =
|
||||||
## Construct JsonNode from tuples and objects.
|
## Construct JsonNode from tuples and objects.
|
||||||
result = newJObject()
|
result = newJObject()
|
||||||
|
|
@ -378,20 +375,20 @@ proc `%`*(o: enum): JsonNode =
|
||||||
## string. Creates a new ``JString JsonNode``.
|
## string. Creates a new ``JString JsonNode``.
|
||||||
result = %($o)
|
result = %($o)
|
||||||
|
|
||||||
proc toJson(x: NimNode): NimNode {.compileTime.} =
|
proc toJsonImpl(x: NimNode): NimNode {.compileTime.} =
|
||||||
case x.kind
|
case x.kind
|
||||||
of nnkBracket: # array
|
of nnkBracket: # array
|
||||||
if x.len == 0: return newCall(bindSym"newJArray")
|
if x.len == 0: return newCall(bindSym"newJArray")
|
||||||
result = newNimNode(nnkBracket)
|
result = newNimNode(nnkBracket)
|
||||||
for i in 0 ..< x.len:
|
for i in 0 ..< x.len:
|
||||||
result.add(toJson(x[i]))
|
result.add(toJsonImpl(x[i]))
|
||||||
result = newCall(bindSym("%", brOpen), result)
|
result = newCall(bindSym("%", brOpen), result)
|
||||||
of nnkTableConstr: # object
|
of nnkTableConstr: # object
|
||||||
if x.len == 0: return newCall(bindSym"newJObject")
|
if x.len == 0: return newCall(bindSym"newJObject")
|
||||||
result = newNimNode(nnkTableConstr)
|
result = newNimNode(nnkTableConstr)
|
||||||
for i in 0 ..< x.len:
|
for i in 0 ..< x.len:
|
||||||
x[i].expectKind nnkExprColonExpr
|
x[i].expectKind nnkExprColonExpr
|
||||||
result.add newTree(nnkExprColonExpr, x[i][0], toJson(x[i][1]))
|
result.add newTree(nnkExprColonExpr, x[i][0], toJsonImpl(x[i][1]))
|
||||||
result = newCall(bindSym("%", brOpen), result)
|
result = newCall(bindSym("%", brOpen), result)
|
||||||
of nnkCurly: # empty object
|
of nnkCurly: # empty object
|
||||||
x.expectLen(0)
|
x.expectLen(0)
|
||||||
|
|
@ -399,7 +396,7 @@ proc toJson(x: NimNode): NimNode {.compileTime.} =
|
||||||
of nnkNilLit:
|
of nnkNilLit:
|
||||||
result = newCall(bindSym"newJNull")
|
result = newCall(bindSym"newJNull")
|
||||||
of nnkPar:
|
of nnkPar:
|
||||||
if x.len == 1: result = toJson(x[0])
|
if x.len == 1: result = toJsonImpl(x[0])
|
||||||
else: result = newCall(bindSym("%", brOpen), x)
|
else: result = newCall(bindSym("%", brOpen), x)
|
||||||
else:
|
else:
|
||||||
result = newCall(bindSym("%", brOpen), x)
|
result = newCall(bindSym("%", brOpen), x)
|
||||||
|
|
@ -407,7 +404,7 @@ proc toJson(x: NimNode): NimNode {.compileTime.} =
|
||||||
macro `%*`*(x: untyped): untyped =
|
macro `%*`*(x: untyped): untyped =
|
||||||
## Convert an expression to a JsonNode directly, without having to specify
|
## Convert an expression to a JsonNode directly, without having to specify
|
||||||
## `%` for every element.
|
## `%` for every element.
|
||||||
result = toJson(x)
|
result = toJsonImpl(x)
|
||||||
|
|
||||||
proc `==`*(a, b: JsonNode): bool =
|
proc `==`*(a, b: JsonNode): bool =
|
||||||
## Check two nodes for equality
|
## Check two nodes for equality
|
||||||
|
|
@ -992,7 +989,6 @@ when defined(nimFixedForwardGeneric):
|
||||||
proc initFromJson[S,T](dst: var array[S,T]; jsonNode: JsonNode; jsonPath: var string)
|
proc initFromJson[S,T](dst: var array[S,T]; jsonNode: JsonNode; jsonPath: var string)
|
||||||
proc initFromJson[T](dst: var Table[string,T]; jsonNode: JsonNode; jsonPath: var string)
|
proc initFromJson[T](dst: var Table[string,T]; jsonNode: JsonNode; jsonPath: var string)
|
||||||
proc initFromJson[T](dst: var OrderedTable[string,T]; jsonNode: JsonNode; jsonPath: var string)
|
proc initFromJson[T](dst: var OrderedTable[string,T]; jsonNode: JsonNode; jsonPath: var string)
|
||||||
proc initFromJson(dst: var StringTableRef; jsonNode: JsonNode; jsonPath: var string)
|
|
||||||
proc initFromJson[T](dst: var ref T; jsonNode: JsonNode; jsonPath: var string)
|
proc initFromJson[T](dst: var ref T; jsonNode: JsonNode; jsonPath: var string)
|
||||||
proc initFromJson[T](dst: var Option[T]; jsonNode: JsonNode; jsonPath: var string)
|
proc initFromJson[T](dst: var Option[T]; jsonNode: JsonNode; jsonPath: var string)
|
||||||
proc initFromJson[T: distinct](dst: var T; jsonNode: JsonNode; jsonPath: var string)
|
proc initFromJson[T: distinct](dst: var T; jsonNode: JsonNode; jsonPath: var string)
|
||||||
|
|
@ -1073,20 +1069,6 @@ when defined(nimFixedForwardGeneric):
|
||||||
initFromJson(mgetOrPut(dst, key, default(T)), jsonNode[key], jsonPath)
|
initFromJson(mgetOrPut(dst, key, default(T)), jsonNode[key], jsonPath)
|
||||||
jsonPath.setLen originalJsonPathLen
|
jsonPath.setLen originalJsonPathLen
|
||||||
|
|
||||||
proc mgetOrPut(tab: var StringTableRef, key: string): var string =
|
|
||||||
if not tab.hasKey(key): tab[key] = ""
|
|
||||||
result = tab[key]
|
|
||||||
|
|
||||||
proc initFromJson(dst: var StringTableRef; jsonNode: JsonNode; jsonPath: var string) =
|
|
||||||
dst = newStringTable(parseEnum[StringTableMode](jsonNode["mode"].getStr))
|
|
||||||
verifyJsonKind(jsonNode, {JObject}, jsonPath)
|
|
||||||
let originalJsonPathLen = jsonPath.len
|
|
||||||
for key in keys(jsonNode["data"].fields):
|
|
||||||
jsonPath.add '.'
|
|
||||||
jsonPath.add key
|
|
||||||
initFromJson(mgetOrPut(dst, key), jsonNode[key], jsonPath)
|
|
||||||
jsonPath.setLen originalJsonPathLen
|
|
||||||
|
|
||||||
proc initFromJson[T](dst: var ref T; jsonNode: JsonNode; jsonPath: var string) =
|
proc initFromJson[T](dst: var ref T; jsonNode: JsonNode; jsonPath: var string) =
|
||||||
verifyJsonKind(jsonNode, {JObject, JNull}, jsonPath)
|
verifyJsonKind(jsonNode, {JObject, JNull}, jsonPath)
|
||||||
if jsonNode.kind == JNull:
|
if jsonNode.kind == JNull:
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ type
|
||||||
StringTableObj* = object of RootObj
|
StringTableObj* = object of RootObj
|
||||||
counter: int
|
counter: int
|
||||||
data: KeyValuePairSeq
|
data: KeyValuePairSeq
|
||||||
mode*: StringTableMode
|
mode: StringTableMode
|
||||||
|
|
||||||
StringTableRef* = ref StringTableObj
|
StringTableRef* = ref StringTableObj
|
||||||
|
|
||||||
|
|
@ -419,6 +419,24 @@ proc `%`*(f: string, t: StringTableRef, flags: set[FormatFlag] = {}): string {.
|
||||||
add(result, f[i])
|
add(result, f[i])
|
||||||
inc(i)
|
inc(i)
|
||||||
|
|
||||||
|
since (1,3,5):
|
||||||
|
proc fromJsonHook*[T](a: var StringTableRef, b: T) =
|
||||||
|
## for json.fromJson
|
||||||
|
mixin jsonTo
|
||||||
|
var mode = jsonTo(b["mode"], StringTableMode)
|
||||||
|
a = newStringTable(mode)
|
||||||
|
let b2 = b["table"]
|
||||||
|
for k,v in b2: a[k] = jsonTo(v, string)
|
||||||
|
|
||||||
|
proc toJsonHook*[](a: StringTableRef): auto =
|
||||||
|
## for json.toJson
|
||||||
|
mixin newJObject
|
||||||
|
mixin toJson
|
||||||
|
result = newJObject()
|
||||||
|
result["mode"] = toJson($a.mode)
|
||||||
|
let t = newJObject()
|
||||||
|
for k,v in a: t[k] = toJson(v)
|
||||||
|
result["table"] = t
|
||||||
|
|
||||||
when isMainModule:
|
when isMainModule:
|
||||||
var x = {"k": "v", "11": "22", "565": "67"}.newStringTable
|
var x = {"k": "v", "11": "22", "565": "67"}.newStringTable
|
||||||
|
|
|
||||||
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
|
||||||
44
tests/stdlib/tjsonutils.nim
Normal file
44
tests/stdlib/tjsonutils.nim
Normal file
|
|
@ -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()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue