jsonutils.nim 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  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. doAssert j.jsonTo(type(a)).toJson == j
  14. import std/[json,strutils,tables,sets,strtabs,options]
  15. #[
  16. Future directions:
  17. add a way to customize serialization, for e.g.:
  18. * field renaming
  19. * allow serializing `enum` and `char` as `string` instead of `int`
  20. (enum is more compact/efficient, and robust to enum renamings, but string
  21. is more human readable)
  22. * handle cyclic references, using a cache of already visited addresses
  23. * implement support for serialization and de-serialization of nested variant
  24. objects.
  25. ]#
  26. import std/macros
  27. type
  28. Joptions* = object
  29. ## Options controlling the behavior of `fromJson`.
  30. allowExtraKeys*: bool
  31. ## If `true` Nim's object to which the JSON is parsed is not required to
  32. ## have a field for every JSON key.
  33. allowMissingKeys*: bool
  34. ## If `true` Nim's object to which JSON is parsed is allowed to have
  35. ## fields without corresponding JSON keys.
  36. # in future work: a key rename could be added
  37. proc isNamedTuple(T: typedesc): bool {.magic: "TypeTrait".}
  38. proc distinctBase(T: typedesc): typedesc {.magic: "TypeTrait".}
  39. template distinctBase[T](a: T): untyped = distinctBase(type(a))(a)
  40. macro getDiscriminants(a: typedesc): seq[string] =
  41. ## return the discriminant keys
  42. # candidate for std/typetraits
  43. var a = a.getTypeImpl
  44. doAssert a.kind == nnkBracketExpr
  45. let sym = a[1]
  46. let t = sym.getTypeImpl
  47. let t2 = t[2]
  48. doAssert t2.kind == nnkRecList
  49. result = newTree(nnkBracket)
  50. for ti in t2:
  51. if ti.kind == nnkRecCase:
  52. let key = ti[0][0]
  53. let typ = ti[0][1]
  54. result.add newLit key.strVal
  55. if result.len > 0:
  56. result = quote do:
  57. @`result`
  58. else:
  59. result = quote do:
  60. seq[string].default
  61. macro initCaseObject(T: typedesc, fun: untyped): untyped =
  62. ## does the minimum to construct a valid case object, only initializing
  63. ## the discriminant fields; see also `getDiscriminants`
  64. # maybe candidate for std/typetraits
  65. var a = T.getTypeImpl
  66. doAssert a.kind == nnkBracketExpr
  67. let sym = a[1]
  68. let t = sym.getTypeImpl
  69. var t2: NimNode
  70. case t.kind
  71. of nnkObjectTy: t2 = t[2]
  72. of nnkRefTy: t2 = t[0].getTypeImpl[2]
  73. else: doAssert false, $t.kind # xxx `nnkPtrTy` could be handled too
  74. doAssert t2.kind == nnkRecList
  75. result = newTree(nnkObjConstr)
  76. result.add sym
  77. for ti in t2:
  78. if ti.kind == nnkRecCase:
  79. let key = ti[0][0]
  80. let typ = ti[0][1]
  81. let key2 = key.strVal
  82. let val = quote do:
  83. `fun`(`key2`, typedesc[`typ`])
  84. result.add newTree(nnkExprColonExpr, key, val)
  85. proc checkJsonImpl(cond: bool, condStr: string, msg = "") =
  86. if not cond:
  87. # just pick 1 exception type for simplicity; other choices would be:
  88. # JsonError, JsonParser, JsonKindError
  89. raise newException(ValueError, msg)
  90. template checkJson(cond: untyped, msg = "") =
  91. checkJsonImpl(cond, astToStr(cond), msg)
  92. proc hasField[T](obj: T, field: string): bool =
  93. for k, _ in fieldPairs(obj):
  94. if k == field:
  95. return true
  96. return false
  97. macro accessField(obj: typed, name: static string): untyped =
  98. newDotExpr(obj, ident(name))
  99. template fromJsonFields(newObj, oldObj, json, discKeys, opt) =
  100. type T = typeof(newObj)
  101. # we could customize whether to allow JNull
  102. checkJson json.kind == JObject, $json.kind
  103. var num, numMatched = 0
  104. for key, val in fieldPairs(newObj):
  105. num.inc
  106. when key notin discKeys:
  107. if json.hasKey key:
  108. numMatched.inc
  109. fromJson(val, json[key])
  110. elif opt.allowMissingKeys:
  111. # if there are no discriminant keys the `oldObj` must always have the
  112. # same keys as the new one. Otherwise we must check, because they could
  113. # be set to different branches.
  114. when typeof(oldObj) isnot typeof(nil):
  115. if discKeys.len == 0 or hasField(oldObj, key):
  116. val = accessField(oldObj, key)
  117. else:
  118. checkJson false, $($T, key, json)
  119. else:
  120. if json.hasKey key:
  121. numMatched.inc
  122. let ok =
  123. if opt.allowExtraKeys and opt.allowMissingKeys:
  124. true
  125. elif opt.allowExtraKeys:
  126. # This check is redundant because if here missing keys are not allowed,
  127. # and if `num != numMatched` it will fail in the loop above but it is left
  128. # for clarity.
  129. assert num == numMatched
  130. num == numMatched
  131. elif opt.allowMissingKeys:
  132. json.len == numMatched
  133. else:
  134. json.len == num and num == numMatched
  135. checkJson ok, $(json.len, num, numMatched, $T, json)
  136. proc fromJson*[T](a: var T, b: JsonNode, opt = Joptions())
  137. proc discKeyMatch[T](obj: T, json: JsonNode, key: static string): bool =
  138. if not json.hasKey key:
  139. return true
  140. let field = accessField(obj, key)
  141. var jsonVal: typeof(field)
  142. fromJson(jsonVal, json[key])
  143. if jsonVal != field:
  144. return false
  145. return true
  146. macro discKeysMatchBodyGen(obj: typed, json: JsonNode,
  147. keys: static seq[string]): untyped =
  148. result = newStmtList()
  149. let r = ident("result")
  150. for key in keys:
  151. let keyLit = newLit key
  152. result.add quote do:
  153. `r` = `r` and discKeyMatch(`obj`, `json`, `keyLit`)
  154. proc discKeysMatch[T](obj: T, json: JsonNode, keys: static seq[string]): bool =
  155. result = true
  156. discKeysMatchBodyGen(obj, json, keys)
  157. proc fromJson*[T](a: var T, b: JsonNode, opt = Joptions()) =
  158. ## inplace version of `jsonTo`
  159. #[
  160. adding "json path" leading to `b` can be added in future work.
  161. ]#
  162. checkJson b != nil, $($T, b)
  163. when compiles(fromJsonHook(a, b)): fromJsonHook(a, b)
  164. elif T is bool: a = to(b,T)
  165. elif T is enum:
  166. case b.kind
  167. of JInt: a = T(b.getBiggestInt())
  168. of JString: a = parseEnum[T](b.getStr())
  169. else: checkJson false, $($T, " ", b)
  170. elif T is Ordinal: a = T(to(b, int))
  171. elif T is pointer: a = cast[pointer](to(b, int))
  172. elif T is distinct:
  173. when nimvm:
  174. # bug, potentially related to https://github.com/nim-lang/Nim/issues/12282
  175. a = T(jsonTo(b, distinctBase(T)))
  176. else:
  177. a.distinctBase.fromJson(b)
  178. elif T is string|SomeNumber: a = to(b,T)
  179. elif T is JsonNode: a = b
  180. elif T is ref | ptr:
  181. if b.kind == JNull: a = nil
  182. else:
  183. a = T()
  184. fromJson(a[], b)
  185. elif T is array:
  186. checkJson a.len == b.len, $(a.len, b.len, $T)
  187. var i = 0
  188. for ai in mitems(a):
  189. fromJson(ai, b[i])
  190. i.inc
  191. elif T is seq:
  192. a.setLen b.len
  193. for i, val in b.getElems:
  194. fromJson(a[i], val)
  195. elif T is object:
  196. template fun(key, typ): untyped {.used.} =
  197. if b.hasKey key:
  198. jsonTo(b[key], typ)
  199. elif hasField(a, key):
  200. accessField(a, key)
  201. else:
  202. default(typ)
  203. const keys = getDiscriminants(T)
  204. when keys.len == 0:
  205. fromJsonFields(a, nil, b, keys, opt)
  206. else:
  207. if discKeysMatch(a, b, keys):
  208. fromJsonFields(a, nil, b, keys, opt)
  209. else:
  210. var newObj = initCaseObject(T, fun)
  211. fromJsonFields(newObj, a, b, keys, opt)
  212. a = newObj
  213. elif T is tuple:
  214. when isNamedTuple(T):
  215. fromJsonFields(a, nil, b, seq[string].default, opt)
  216. else:
  217. checkJson b.kind == JArray, $(b.kind) # we could customize whether to allow JNull
  218. var i = 0
  219. for val in fields(a):
  220. fromJson(val, b[i])
  221. i.inc
  222. checkJson b.len == i, $(b.len, i, $T, b) # could customize
  223. else:
  224. # checkJson not appropriate here
  225. static: doAssert false, "not yet implemented: " & $T
  226. proc jsonTo*(b: JsonNode, T: typedesc): T =
  227. ## reverse of `toJson`
  228. fromJson(result, b)
  229. proc toJson*[T](a: T): JsonNode =
  230. ## serializes `a` to json; uses `toJsonHook(a: T)` if it's in scope to
  231. ## customize serialization, see strtabs.toJsonHook for an example.
  232. when compiles(toJsonHook(a)): result = toJsonHook(a)
  233. elif T is object | tuple:
  234. when T is object or isNamedTuple(T):
  235. result = newJObject()
  236. for k, v in a.fieldPairs: result[k] = toJson(v)
  237. else:
  238. result = newJArray()
  239. for v in a.fields: result.add toJson(v)
  240. elif T is ref | ptr:
  241. if system.`==`(a, nil): result = newJNull()
  242. else: result = toJson(a[])
  243. elif T is array | seq:
  244. result = newJArray()
  245. for ai in a: result.add toJson(ai)
  246. elif T is pointer: result = toJson(cast[int](a))
  247. # edge case: `a == nil` could've also led to `newJNull()`, but this results
  248. # in simpler code for `toJson` and `fromJson`.
  249. elif T is distinct: result = toJson(a.distinctBase)
  250. elif T is bool: result = %(a)
  251. elif T is Ordinal: result = %(a.ord)
  252. else: result = %a
  253. proc fromJsonHook*[K, V](t: var (Table[K, V] | OrderedTable[K, V]),
  254. jsonNode: JsonNode) =
  255. ## Enables `fromJson` for `Table` and `OrderedTable` types.
  256. ##
  257. ## See also:
  258. ## * `toJsonHook proc<#toJsonHook,(Table[K,V]|OrderedTable[K,V])>`_
  259. runnableExamples:
  260. import tables, json
  261. var foo: tuple[t: Table[string, int], ot: OrderedTable[string, int]]
  262. fromJson(foo, parseJson("""
  263. {"t":{"two":2,"one":1},"ot":{"one":1,"three":3}}"""))
  264. assert foo.t == [("one", 1), ("two", 2)].toTable
  265. assert foo.ot == [("one", 1), ("three", 3)].toOrderedTable
  266. assert jsonNode.kind == JObject,
  267. "The kind of the `jsonNode` must be `JObject`, but its actual " &
  268. "type is `" & $jsonNode.kind & "`."
  269. clear(t)
  270. for k, v in jsonNode:
  271. t[k] = jsonTo(v, V)
  272. proc toJsonHook*[K, V](t: (Table[K, V] | OrderedTable[K, V])): JsonNode =
  273. ## Enables `toJson` for `Table` and `OrderedTable` types.
  274. ##
  275. ## See also:
  276. ## * `fromJsonHook proc<#fromJsonHook,(Table[K,V]|OrderedTable[K,V]),JsonNode>`_
  277. runnableExamples:
  278. import tables, json
  279. let foo = (
  280. t: [("two", 2)].toTable,
  281. ot: [("one", 1), ("three", 3)].toOrderedTable)
  282. assert $toJson(foo) == """{"t":{"two":2},"ot":{"one":1,"three":3}}"""
  283. result = newJObject()
  284. for k, v in pairs(t):
  285. result[k] = toJson(v)
  286. proc fromJsonHook*[A](s: var SomeSet[A], jsonNode: JsonNode) =
  287. ## Enables `fromJson` for `HashSet` and `OrderedSet` types.
  288. ##
  289. ## See also:
  290. ## * `toJsonHook proc<#toJsonHook,SomeSet[A]>`_
  291. runnableExamples:
  292. import sets, json
  293. var foo: tuple[hs: HashSet[string], os: OrderedSet[string]]
  294. fromJson(foo, parseJson("""
  295. {"hs": ["hash", "set"], "os": ["ordered", "set"]}"""))
  296. assert foo.hs == ["hash", "set"].toHashSet
  297. assert foo.os == ["ordered", "set"].toOrderedSet
  298. assert jsonNode.kind == JArray,
  299. "The kind of the `jsonNode` must be `JArray`, but its actual " &
  300. "type is `" & $jsonNode.kind & "`."
  301. clear(s)
  302. for v in jsonNode:
  303. incl(s, jsonTo(v, A))
  304. proc toJsonHook*[A](s: SomeSet[A]): JsonNode =
  305. ## Enables `toJson` for `HashSet` and `OrderedSet` types.
  306. ##
  307. ## See also:
  308. ## * `fromJsonHook proc<#fromJsonHook,SomeSet[A],JsonNode>`_
  309. runnableExamples:
  310. import sets, json
  311. let foo = (hs: ["hash"].toHashSet, os: ["ordered", "set"].toOrderedSet)
  312. assert $toJson(foo) == """{"hs":["hash"],"os":["ordered","set"]}"""
  313. result = newJArray()
  314. for k in s:
  315. add(result, toJson(k))
  316. proc fromJsonHook*[T](self: var Option[T], jsonNode: JsonNode) =
  317. ## Enables `fromJson` for `Option` types.
  318. ##
  319. ## See also:
  320. ## * `toJsonHook proc<#toJsonHook,Option[T]>`_
  321. runnableExamples:
  322. import options, json
  323. var opt: Option[string]
  324. fromJsonHook(opt, parseJson("\"test\""))
  325. assert get(opt) == "test"
  326. fromJson(opt, parseJson("null"))
  327. assert isNone(opt)
  328. if jsonNode.kind != JNull:
  329. self = some(jsonTo(jsonNode, T))
  330. else:
  331. self = none[T]()
  332. proc toJsonHook*[T](self: Option[T]): JsonNode =
  333. ## Enables `toJson` for `Option` types.
  334. ##
  335. ## See also:
  336. ## * `fromJsonHook proc<#fromJsonHook,Option[T],JsonNode>`_
  337. runnableExamples:
  338. import options, json
  339. let optSome = some("test")
  340. assert $toJson(optSome) == "\"test\""
  341. let optNone = none[string]()
  342. assert $toJson(optNone) == "null"
  343. if isSome(self):
  344. toJson(get(self))
  345. else:
  346. newJNull()
  347. proc fromJsonHook*(a: var StringTableRef, b: JsonNode) =
  348. ## Enables `fromJson` for `StringTableRef` type.
  349. ##
  350. ## See also:
  351. ## * `toJsonHook` proc<#toJsonHook,StringTableRef>`_
  352. runnableExamples:
  353. import strtabs, json
  354. var t = newStringTable(modeCaseSensitive)
  355. let jsonStr = """{"mode": 0, "table": {"name": "John", "surname": "Doe"}}"""
  356. fromJsonHook(t, parseJson(jsonStr))
  357. assert t[] == newStringTable("name", "John", "surname", "Doe",
  358. modeCaseSensitive)[]
  359. var mode = jsonTo(b["mode"], StringTableMode)
  360. a = newStringTable(mode)
  361. let b2 = b["table"]
  362. for k,v in b2: a[k] = jsonTo(v, string)
  363. proc toJsonHook*(a: StringTableRef): JsonNode =
  364. ## Enables `toJson` for `StringTableRef` type.
  365. ##
  366. ## See also:
  367. ## * `fromJsonHook` proc<#fromJsonHook,StringTableRef,JsonNode>`_
  368. runnableExamples:
  369. import strtabs, json
  370. let t = newStringTable("name", "John", "surname", "Doe", modeCaseSensitive)
  371. let jsonStr = """{"mode": "modeCaseSensitive",
  372. "table": {"name": "John", "surname": "Doe"}}"""
  373. assert toJson(t) == parseJson(jsonStr)
  374. result = newJObject()
  375. result["mode"] = toJson($a.mode)
  376. let t = newJObject()
  377. for k,v in a: t[k] = toJson(v)
  378. result["table"] = t