jsonutils.nim 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. ##[
  2. This module implements a hookable (de)serialization for arbitrary types.
  3. Design goal: avoid importing modules where a custom serialization is needed;
  4. see strtabs.fromJsonHook,toJsonHook for an example.
  5. ]##
  6. runnableExamples:
  7. import std/[strtabs,json]
  8. type Foo = ref object
  9. t: bool
  10. z1: int8
  11. let a = (1.5'f32, (b: "b2", a: "a2"), 'x', @[Foo(t: true, z1: -3), nil], [{"name": "John"}.newStringTable])
  12. let j = a.toJson
  13. assert j.jsonTo(typeof(a)).toJson == j
  14. assert $[NaN, Inf, -Inf, 0.0, -0.0, 1.0, 1e-2].toJson == """["nan","inf","-inf",0.0,-0.0,1.0,0.01]"""
  15. assert 0.0.toJson.kind == JFloat
  16. assert Inf.toJson.kind == JString
  17. import json, strutils, tables, sets, strtabs, options, strformat
  18. #[
  19. Future directions:
  20. add a way to customize serialization, for e.g.:
  21. * field renaming
  22. * allow serializing `enum` and `char` as `string` instead of `int`
  23. (enum is more compact/efficient, and robust to enum renamings, but string
  24. is more human readable)
  25. * handle cyclic references, using a cache of already visited addresses
  26. * implement support for serialization and de-serialization of nested variant
  27. objects.
  28. ]#
  29. import macros
  30. from enumutils import symbolName
  31. from typetraits import OrdinalEnum, tupleLen
  32. when defined(nimPreviewSlimSystem):
  33. import std/assertions
  34. when not defined(nimFixedForwardGeneric):
  35. # xxx remove pending csources_v1 update >= 1.2.0
  36. proc to[T](node: JsonNode, t: typedesc[T]): T =
  37. when T is string: node.getStr
  38. elif T is bool: node.getBool
  39. else: static: doAssert false, $T # support as needed (only needed during bootstrap)
  40. proc isNamedTuple(T: typedesc): bool = # old implementation
  41. when T isnot tuple: result = false
  42. else:
  43. var t: T
  44. for name, _ in t.fieldPairs:
  45. when name == "Field0": return compiles(t.Field0)
  46. else: return true
  47. return false
  48. else:
  49. proc isNamedTuple(T: typedesc): bool {.magic: "TypeTrait".}
  50. type
  51. Joptions* = object # xxx rename FromJsonOptions
  52. ## Options controlling the behavior of `fromJson`.
  53. allowExtraKeys*: bool
  54. ## If `true` Nim's object to which the JSON is parsed is not required to
  55. ## have a field for every JSON key.
  56. allowMissingKeys*: bool
  57. ## If `true` Nim's object to which JSON is parsed is allowed to have
  58. ## fields without corresponding JSON keys.
  59. # in future work: a key rename could be added
  60. EnumMode* = enum
  61. joptEnumOrd
  62. joptEnumSymbol
  63. joptEnumString
  64. JsonNodeMode* = enum ## controls `toJson` for JsonNode types
  65. joptJsonNodeAsRef ## returns the ref as is
  66. joptJsonNodeAsCopy ## returns a deep copy of the JsonNode
  67. joptJsonNodeAsObject ## treats JsonNode as a regular ref object
  68. ToJsonOptions* = object
  69. enumMode*: EnumMode
  70. jsonNodeMode*: JsonNodeMode
  71. # xxx charMode, etc
  72. proc initToJsonOptions*(): ToJsonOptions =
  73. ## initializes `ToJsonOptions` with sane options.
  74. ToJsonOptions(enumMode: joptEnumOrd, jsonNodeMode: joptJsonNodeAsRef)
  75. proc distinctBase(T: typedesc, recursive: static bool = true): typedesc {.magic: "TypeTrait".}
  76. template distinctBase[T](a: T, recursive: static bool = true): untyped = distinctBase(typeof(a), recursive)(a)
  77. macro getDiscriminants(a: typedesc): seq[string] =
  78. ## return the discriminant keys
  79. # candidate for std/typetraits
  80. var a = a.getTypeImpl
  81. doAssert a.kind == nnkBracketExpr
  82. let sym = a[1]
  83. let t = sym.getTypeImpl
  84. let t2 = t[2]
  85. doAssert t2.kind == nnkRecList
  86. result = newTree(nnkBracket)
  87. for ti in t2:
  88. if ti.kind == nnkRecCase:
  89. let key = ti[0][0]
  90. let typ = ti[0][1]
  91. result.add newLit key.strVal
  92. if result.len > 0:
  93. result = quote do:
  94. @`result`
  95. else:
  96. result = quote do:
  97. seq[string].default
  98. macro initCaseObject(T: typedesc, fun: untyped): untyped =
  99. ## does the minimum to construct a valid case object, only initializing
  100. ## the discriminant fields; see also `getDiscriminants`
  101. # maybe candidate for std/typetraits
  102. var a = T.getTypeImpl
  103. doAssert a.kind == nnkBracketExpr
  104. let sym = a[1]
  105. let t = sym.getTypeImpl
  106. var t2: NimNode
  107. case t.kind
  108. of nnkObjectTy: t2 = t[2]
  109. of nnkRefTy: t2 = t[0].getTypeImpl[2]
  110. else: doAssert false, $t.kind # xxx `nnkPtrTy` could be handled too
  111. doAssert t2.kind == nnkRecList
  112. result = newTree(nnkObjConstr)
  113. result.add sym
  114. for ti in t2:
  115. if ti.kind == nnkRecCase:
  116. let key = ti[0][0]
  117. let typ = ti[0][1]
  118. let key2 = key.strVal
  119. let val = quote do:
  120. `fun`(`key2`, typedesc[`typ`])
  121. result.add newTree(nnkExprColonExpr, key, val)
  122. proc raiseJsonException(condStr: string, msg: string) {.noinline.} =
  123. # just pick 1 exception type for simplicity; other choices would be:
  124. # JsonError, JsonParser, JsonKindError
  125. raise newException(ValueError, condStr & " failed: " & msg)
  126. template checkJson(cond: untyped, msg = "") =
  127. if not cond:
  128. raiseJsonException(astToStr(cond), msg)
  129. proc hasField[T](obj: T, field: string): bool =
  130. for k, _ in fieldPairs(obj):
  131. if k == field:
  132. return true
  133. return false
  134. macro accessField(obj: typed, name: static string): untyped =
  135. newDotExpr(obj, ident(name))
  136. template fromJsonFields(newObj, oldObj, json, discKeys, opt) =
  137. type T = typeof(newObj)
  138. # we could customize whether to allow JNull
  139. checkJson json.kind == JObject, $json.kind
  140. var num, numMatched = 0
  141. for key, val in fieldPairs(newObj):
  142. num.inc
  143. when key notin discKeys:
  144. if json.hasKey key:
  145. numMatched.inc
  146. fromJson(val, json[key], opt)
  147. elif opt.allowMissingKeys:
  148. # if there are no discriminant keys the `oldObj` must always have the
  149. # same keys as the new one. Otherwise we must check, because they could
  150. # be set to different branches.
  151. when typeof(oldObj) isnot typeof(nil):
  152. if discKeys.len == 0 or hasField(oldObj, key):
  153. val = accessField(oldObj, key)
  154. else:
  155. checkJson false, "key '$1' for $2 not in $3" % [key, $T, json.pretty()]
  156. else:
  157. if json.hasKey key:
  158. numMatched.inc
  159. let ok =
  160. if opt.allowExtraKeys and opt.allowMissingKeys:
  161. true
  162. elif opt.allowExtraKeys:
  163. # This check is redundant because if here missing keys are not allowed,
  164. # and if `num != numMatched` it will fail in the loop above but it is left
  165. # for clarity.
  166. assert num == numMatched
  167. num == numMatched
  168. elif opt.allowMissingKeys:
  169. json.len == numMatched
  170. else:
  171. json.len == num and num == numMatched
  172. checkJson ok, "There were $1 keys (expecting $2) for $3 with $4" % [$json.len, $num, $T, json.pretty()]
  173. proc fromJson*[T](a: var T, b: JsonNode, opt = Joptions())
  174. proc discKeyMatch[T](obj: T, json: JsonNode, key: static string): bool =
  175. if not json.hasKey key:
  176. return true
  177. let field = accessField(obj, key)
  178. var jsonVal: typeof(field)
  179. fromJson(jsonVal, json[key])
  180. if jsonVal != field:
  181. return false
  182. return true
  183. macro discKeysMatchBodyGen(obj: typed, json: JsonNode,
  184. keys: static seq[string]): untyped =
  185. result = newStmtList()
  186. let r = ident("result")
  187. for key in keys:
  188. let keyLit = newLit key
  189. result.add quote do:
  190. `r` = `r` and discKeyMatch(`obj`, `json`, `keyLit`)
  191. proc discKeysMatch[T](obj: T, json: JsonNode, keys: static seq[string]): bool =
  192. result = true
  193. discKeysMatchBodyGen(obj, json, keys)
  194. proc fromJson*[T](a: var T, b: JsonNode, opt = Joptions()) =
  195. ## inplace version of `jsonTo`
  196. #[
  197. adding "json path" leading to `b` can be added in future work.
  198. ]#
  199. checkJson b != nil, $($T, b)
  200. when compiles(fromJsonHook(a, b, opt)): fromJsonHook(a, b, opt)
  201. elif compiles(fromJsonHook(a, b)): fromJsonHook(a, b)
  202. elif T is bool: a = to(b,T)
  203. elif T is enum:
  204. case b.kind
  205. of JInt: a = T(b.getBiggestInt())
  206. of JString: a = parseEnum[T](b.getStr())
  207. else: checkJson false, fmt"Expecting int/string for {$T} got {b.pretty()}"
  208. elif T is uint|uint64: a = T(to(b, uint64))
  209. elif T is Ordinal: a = cast[T](to(b, int))
  210. elif T is pointer: a = cast[pointer](to(b, int))
  211. elif T is distinct:
  212. when nimvm:
  213. # bug, potentially related to https://github.com/nim-lang/Nim/issues/12282
  214. a = T(jsonTo(b, distinctBase(T)))
  215. else:
  216. a.distinctBase.fromJson(b)
  217. elif T is string|SomeNumber: a = to(b,T)
  218. elif T is cstring:
  219. case b.kind
  220. of JNull: a = nil
  221. of JString: a = b.str
  222. else: checkJson false, fmt"Expecting null/string for {$T} got {b.pretty()}"
  223. elif T is JsonNode: a = b
  224. elif T is ref | ptr:
  225. if b.kind == JNull: a = nil
  226. else:
  227. a = T()
  228. fromJson(a[], b, opt)
  229. elif T is array:
  230. checkJson a.len == b.len, fmt"Json array size doesn't match for {$T}"
  231. var i = 0
  232. for ai in mitems(a):
  233. fromJson(ai, b[i], opt)
  234. i.inc
  235. elif T is set:
  236. type E = typeof(for ai in a: ai)
  237. for val in b.getElems:
  238. incl a, jsonTo(val, E)
  239. elif T is seq:
  240. a.setLen b.len
  241. for i, val in b.getElems:
  242. fromJson(a[i], val, opt)
  243. elif T is object:
  244. template fun(key, typ): untyped {.used.} =
  245. if b.hasKey key:
  246. jsonTo(b[key], typ)
  247. elif hasField(a, key):
  248. accessField(a, key)
  249. else:
  250. default(typ)
  251. const keys = getDiscriminants(T)
  252. when keys.len == 0:
  253. fromJsonFields(a, nil, b, keys, opt)
  254. else:
  255. if discKeysMatch(a, b, keys):
  256. fromJsonFields(a, nil, b, keys, opt)
  257. else:
  258. var newObj = initCaseObject(T, fun)
  259. fromJsonFields(newObj, a, b, keys, opt)
  260. a = newObj
  261. elif T is tuple:
  262. when isNamedTuple(T):
  263. fromJsonFields(a, nil, b, seq[string].default, opt)
  264. else:
  265. checkJson b.kind == JArray, $(b.kind) # we could customize whether to allow JNull
  266. when compiles(tupleLen(T)):
  267. let tupleSize = tupleLen(T)
  268. else:
  269. # Tuple len isn't in csources_v1 so using tupleLen would fail.
  270. # Else branch basically never runs (tupleLen added in 1.1 and jsonutils in 1.4), but here for consistency
  271. var tupleSize = 0
  272. for val in fields(a):
  273. tupleSize.inc
  274. checkJson b.len == tupleSize, fmt"Json doesn't match expected length of {tupleSize}, got {b.pretty()}"
  275. var i = 0
  276. for val in fields(a):
  277. fromJson(val, b[i], opt)
  278. i.inc
  279. else:
  280. # checkJson not appropriate here
  281. static: doAssert false, "not yet implemented: " & $T
  282. proc jsonTo*(b: JsonNode, T: typedesc, opt = Joptions()): T =
  283. ## reverse of `toJson`
  284. fromJson(result, b, opt)
  285. proc toJson*[T](a: T, opt = initToJsonOptions()): JsonNode =
  286. ## serializes `a` to json; uses `toJsonHook(a: T)` if it's in scope to
  287. ## customize serialization, see strtabs.toJsonHook for an example.
  288. ##
  289. ## .. note:: With `-d:nimPreviewJsonutilsHoleyEnum`, `toJson` now can
  290. ## serialize/deserialize holey enums as regular enums (via `ord`) instead of as strings.
  291. ## It is expected that this behavior becomes the new default in upcoming versions.
  292. when compiles(toJsonHook(a, opt)): result = toJsonHook(a, opt)
  293. elif compiles(toJsonHook(a)): result = toJsonHook(a)
  294. elif T is object | tuple:
  295. when T is object or isNamedTuple(T):
  296. result = newJObject()
  297. for k, v in a.fieldPairs: result[k] = toJson(v, opt)
  298. else:
  299. result = newJArray()
  300. for v in a.fields: result.add toJson(v, opt)
  301. elif T is ref | ptr:
  302. template impl =
  303. if system.`==`(a, nil): result = newJNull()
  304. else: result = toJson(a[], opt)
  305. when T is JsonNode:
  306. case opt.jsonNodeMode
  307. of joptJsonNodeAsRef: result = a
  308. of joptJsonNodeAsCopy: result = copy(a)
  309. of joptJsonNodeAsObject: impl()
  310. else: impl()
  311. elif T is array | seq | set:
  312. result = newJArray()
  313. for ai in a: result.add toJson(ai, opt)
  314. elif T is pointer: result = toJson(cast[int](a), opt)
  315. # edge case: `a == nil` could've also led to `newJNull()`, but this results
  316. # in simpler code for `toJson` and `fromJson`.
  317. elif T is distinct: result = toJson(a.distinctBase, opt)
  318. elif T is bool: result = %(a)
  319. elif T is SomeInteger: result = %a
  320. elif T is enum:
  321. case opt.enumMode
  322. of joptEnumOrd:
  323. when T is Ordinal or defined(nimPreviewJsonutilsHoleyEnum): %(a.ord)
  324. else: toJson($a, opt)
  325. of joptEnumSymbol:
  326. when T is OrdinalEnum:
  327. toJson(symbolName(a), opt)
  328. else:
  329. toJson($a, opt)
  330. of joptEnumString: toJson($a, opt)
  331. elif T is Ordinal: result = %(a.ord)
  332. elif T is cstring: (if a == nil: result = newJNull() else: result = % $a)
  333. else: result = %a
  334. proc fromJsonHook*[K: string|cstring, V](t: var (Table[K, V] | OrderedTable[K, V]),
  335. jsonNode: JsonNode, opt = Joptions()) =
  336. ## Enables `fromJson` for `Table` and `OrderedTable` types.
  337. ##
  338. ## See also:
  339. ## * `toJsonHook proc<#toJsonHook>`_
  340. runnableExamples:
  341. import std/[tables, json]
  342. var foo: tuple[t: Table[string, int], ot: OrderedTable[string, int]]
  343. fromJson(foo, parseJson("""
  344. {"t":{"two":2,"one":1},"ot":{"one":1,"three":3}}"""))
  345. assert foo.t == [("one", 1), ("two", 2)].toTable
  346. assert foo.ot == [("one", 1), ("three", 3)].toOrderedTable
  347. assert jsonNode.kind == JObject,
  348. "The kind of the `jsonNode` must be `JObject`, but its actual " &
  349. "type is `" & $jsonNode.kind & "`."
  350. clear(t)
  351. for k, v in jsonNode:
  352. t[k] = jsonTo(v, V, opt)
  353. proc toJsonHook*[K: string|cstring, V](t: (Table[K, V] | OrderedTable[K, V]), opt = initToJsonOptions()): JsonNode =
  354. ## Enables `toJson` for `Table` and `OrderedTable` types.
  355. ##
  356. ## See also:
  357. ## * `fromJsonHook proc<#fromJsonHook,,JsonNode>`_
  358. # pending PR #9217 use: toSeq(a) instead of `collect` in `runnableExamples`.
  359. runnableExamples:
  360. import std/[tables, json, sugar]
  361. let foo = (
  362. t: [("two", 2)].toTable,
  363. ot: [("one", 1), ("three", 3)].toOrderedTable)
  364. assert $toJson(foo) == """{"t":{"two":2},"ot":{"one":1,"three":3}}"""
  365. # if keys are not string|cstring, you can use this:
  366. let a = {10: "foo", 11: "bar"}.newOrderedTable
  367. let a2 = collect: (for k,v in a: (k,v))
  368. assert $toJson(a2) == """[[10,"foo"],[11,"bar"]]"""
  369. result = newJObject()
  370. for k, v in pairs(t):
  371. # not sure if $k has overhead for string
  372. result[(when K is string: k else: $k)] = toJson(v, opt)
  373. proc fromJsonHook*[A](s: var SomeSet[A], jsonNode: JsonNode, opt = Joptions()) =
  374. ## Enables `fromJson` for `HashSet` and `OrderedSet` types.
  375. ##
  376. ## See also:
  377. ## * `toJsonHook proc<#toJsonHook,SomeSet[A]>`_
  378. runnableExamples:
  379. import std/[sets, json]
  380. var foo: tuple[hs: HashSet[string], os: OrderedSet[string]]
  381. fromJson(foo, parseJson("""
  382. {"hs": ["hash", "set"], "os": ["ordered", "set"]}"""))
  383. assert foo.hs == ["hash", "set"].toHashSet
  384. assert foo.os == ["ordered", "set"].toOrderedSet
  385. assert jsonNode.kind == JArray,
  386. "The kind of the `jsonNode` must be `JArray`, but its actual " &
  387. "type is `" & $jsonNode.kind & "`."
  388. clear(s)
  389. for v in jsonNode:
  390. incl(s, jsonTo(v, A, opt))
  391. proc toJsonHook*[A](s: SomeSet[A], opt = initToJsonOptions()): JsonNode =
  392. ## Enables `toJson` for `HashSet` and `OrderedSet` types.
  393. ##
  394. ## See also:
  395. ## * `fromJsonHook proc<#fromJsonHook,SomeSet[A],JsonNode>`_
  396. runnableExamples:
  397. import std/[sets, json]
  398. let foo = (hs: ["hash"].toHashSet, os: ["ordered", "set"].toOrderedSet)
  399. assert $toJson(foo) == """{"hs":["hash"],"os":["ordered","set"]}"""
  400. result = newJArray()
  401. for k in s:
  402. add(result, toJson(k, opt))
  403. proc fromJsonHook*[T](self: var Option[T], jsonNode: JsonNode, opt = Joptions()) =
  404. ## Enables `fromJson` for `Option` types.
  405. ##
  406. ## See also:
  407. ## * `toJsonHook proc<#toJsonHook,Option[T]>`_
  408. runnableExamples:
  409. import std/[options, json]
  410. var opt: Option[string]
  411. fromJsonHook(opt, parseJson("\"test\""))
  412. assert get(opt) == "test"
  413. fromJson(opt, parseJson("null"))
  414. assert isNone(opt)
  415. if jsonNode.kind != JNull:
  416. self = some(jsonTo(jsonNode, T, opt))
  417. else:
  418. self = none[T]()
  419. proc toJsonHook*[T](self: Option[T], opt = initToJsonOptions()): JsonNode =
  420. ## Enables `toJson` for `Option` types.
  421. ##
  422. ## See also:
  423. ## * `fromJsonHook proc<#fromJsonHook,Option[T],JsonNode>`_
  424. runnableExamples:
  425. import std/[options, json]
  426. let optSome = some("test")
  427. assert $toJson(optSome) == "\"test\""
  428. let optNone = none[string]()
  429. assert $toJson(optNone) == "null"
  430. if isSome(self):
  431. toJson(get(self), opt)
  432. else:
  433. newJNull()
  434. proc fromJsonHook*(a: var StringTableRef, b: JsonNode) =
  435. ## Enables `fromJson` for `StringTableRef` type.
  436. ##
  437. ## See also:
  438. ## * `toJsonHook proc<#toJsonHook,StringTableRef>`_
  439. runnableExamples:
  440. import std/[strtabs, json]
  441. var t = newStringTable(modeCaseSensitive)
  442. let jsonStr = """{"mode": 0, "table": {"name": "John", "surname": "Doe"}}"""
  443. fromJsonHook(t, parseJson(jsonStr))
  444. assert t[] == newStringTable("name", "John", "surname", "Doe",
  445. modeCaseSensitive)[]
  446. var mode = jsonTo(b["mode"], StringTableMode)
  447. a = newStringTable(mode)
  448. let b2 = b["table"]
  449. for k,v in b2: a[k] = jsonTo(v, string)
  450. proc toJsonHook*(a: StringTableRef): JsonNode =
  451. ## Enables `toJson` for `StringTableRef` type.
  452. ##
  453. ## See also:
  454. ## * `fromJsonHook proc<#fromJsonHook,StringTableRef,JsonNode>`_
  455. runnableExamples:
  456. import std/[strtabs, json]
  457. let t = newStringTable("name", "John", "surname", "Doe", modeCaseSensitive)
  458. let jsonStr = """{"mode": "modeCaseSensitive",
  459. "table": {"name": "John", "surname": "Doe"}}"""
  460. assert toJson(t) == parseJson(jsonStr)
  461. result = newJObject()
  462. result["mode"] = toJson($a.mode)
  463. let t = newJObject()
  464. for k,v in a: t[k] = toJson(v)
  465. result["table"] = t