diff --git a/changelog.md b/changelog.md index 25fc3535c7eea..95353793e7fd0 100644 --- a/changelog.md +++ b/changelog.md @@ -99,6 +99,8 @@ - `osproc.execCmdEx` now takes an optional `input` for stdin, `workingDir` and `env` parameters. +- added `json.jsonTo,toJson` for json serialization/deserialization from custom types. + ## 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: diff --git a/lib/pure/json.nim b/lib/pure/json.nim index c420ee6ca1283..4cbdb1a7fb72b 100644 --- a/lib/pure/json.nim +++ b/lib/pure/json.nim @@ -149,8 +149,9 @@ runnableExamples: doAssert $(%* Foo()) == """{"a1":0,"a2":0,"a0":0,"a3":0,"a4":0}""" import - hashes, tables, strutils, lexbase, streams, macros, parsejson, - options + hashes, tables, strutils, lexbase, streams, macros, parsejson + +import options # xxx remove this dependency using same approach as https://github.com/nim-lang/Nim/pull/14563 import std/private/since export @@ -1266,11 +1267,88 @@ when false: # To get that we shall use, obj["json"] 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) {.since: (1,3,5).} = + ## inplace version of `jsonTo` + #[ + adding "json path" leading to `b` can be added in future work. + ]# + checkJson b != nil, $($T, b) + when false: discard + elif 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: 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 {.since: (1,3,5).} = + ## reverse of `toJson` + fromJson(result, b) proc toJson*[T](a: T): JsonNode {.since: (1,3,5).} = ## like `%` but allows custom serialization hook if `serialize(a: T)` is in scope - when compiles(toJsonHook(a)): - result = toJsonHook(a) + when false: discard + elif 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: @@ -1284,18 +1362,12 @@ proc toJson*[T](a: T): JsonNode {.since: (1,3,5).} = 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 = %(cast[int](a)) - else: - result = %a + 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 = %(cast[int](a)) + else: result = %a when isMainModule: # Note: Macro tests are in tests/stdlib/tjsonmacro.nim diff --git a/lib/pure/strtabs.nim b/lib/pure/strtabs.nim index cc1df7b0cd12f..81ff7fbdeb514 100644 --- a/lib/pure/strtabs.nim +++ b/lib/pure/strtabs.nim @@ -419,16 +419,24 @@ proc `%`*(f: string, t: StringTableRef, flags: set[FormatFlag] = {}): string {. add(result, f[i]) inc(i) -proc toJsonHook*[](a: StringTableRef): auto = - ## allows a custom serializer (eg json) to serialize this as we want. - 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 +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: var x = {"k": "v", "11": "22", "565": "67"}.newStringTable diff --git a/tests/stdlib/tjsonmacro.nim b/tests/stdlib/tjsonmacro.nim index e2ac657843243..62ae46a5703d1 100644 --- a/tests/stdlib/tjsonmacro.nim +++ b/tests/stdlib/tjsonmacro.nim @@ -639,18 +639,25 @@ static: import strtabs -proc testCustom()= - var t = {"name": "John", "city": "Monaco"}.newStringTable - let s = toJson(t) - doAssert $s == """{"mode":"modeCaseSensitive","table":{"city":"Monaco","name":"John"}}""", $s - -proc testToJson() = - var t = {"z": "Z", "y": "Y"}.newStringTable - type A = ref object - a1: string - let a = (1.1, "fo", 'x', @[10,11], [true, false], [t,newStringTable()], (foo: 0.5'f32, bar: A(a1: "abc"), bar2: A.default)) - let j = a.toJson - doAssert $j == """[1.1,"fo",120,[10,11],[true,false],[{"mode":"modeCaseSensitive","table":{"y":"Y","z":"Z"}},{"mode":"modeCaseSensitive","table":{}}],{"foo":0.5,"bar":{"a1":"abc"},"bar2":null}]""" - -testCustom() -testToJson() +# xxx move to `tjson` pending https://github.com/nim-lang/Nim/pull/14572 +proc testRoundtrip[T](t: T, expected: string) = + let j = t.toJson + doAssert $j == expected, $j + doAssert j.jsonTo(T).toJson == j + +block: # toJson, jsonTo + type Foo = distinct float + testRoundtrip('x', """120""") + when not defined(js): + testRoundtrip(cast[pointer](12345)): """12345""" + testRoundtrip(Foo(1.5)): """1.5""" + testRoundtrip({"z": "Z", "y": "Y"}.toOrderedTable): """{"z":"Z","y":"Y"}""" + testRoundtrip({"z": (f1: 'f'), }.toTable): """{"z":{"f1":102}}""" + 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'u8,3'u8], (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],{"foo":0.5,"bar":{"a1":"abc"},"bar2":null}]"""