summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--changelog.md14
-rw-r--r--lib/pure/collections/sets.nim4
-rw-r--r--lib/pure/collections/tables.nim4
-rw-r--r--lib/pure/options.nim1
-rw-r--r--lib/pure/strtabs.nim20
-rw-r--r--lib/std/jsonutils.nim278
-rw-r--r--tests/stdlib/tjsonutils.nim185
7 files changed, 447 insertions, 59 deletions
diff --git a/changelog.md b/changelog.md
index 2f7b584a3..3333a837b 100644
--- a/changelog.md
+++ b/changelog.md
@@ -4,6 +4,20 @@
 
 ## Standard library additions and changes
 
+- Added some enhancements to `std/jsonutils` module.
+  * Added a possibility to deserialize JSON arrays directly to `HashSet` and
+    `OrderedSet` types and respectively to serialize those types to JSON arrays
+    via `jsonutils.fromJson` and `jsonutils.toJson` procedures.
+  * Added a possibility to deserialize JSON `null` objects to Nim option objects
+    and respectively to serialize Nim option object to JSON object if `isSome`
+    or to JSON null object if `isNone` via `jsonutils.fromJson` and
+    `jsonutils.toJson` procedures.
+  * Added `Joptions` parameter to `jsonutils.fromJson` procedure currently
+    containing two boolean options `allowExtraKeys` and `allowMissingKeys`.
+    - If `allowExtraKeys` is `true` Nim's object to which the JSON is parsed is
+      not required to have a field for every JSON key.
+    - If `allowMissingKeys` is `true` Nim's object to which JSON is parsed is
+      allowed to have fields without corresponding JSON keys.
 - Added `bindParams`, `bindParam` to `db_sqlite` for binding parameters into a `SqlPrepared` statement.
 - Add `tryInsert`,`insert` procs to `db_*` libs accept primary key column name.
 - Added `xmltree.newVerbatimText` support create `style`'s,`script`'s text.
diff --git a/lib/pure/collections/sets.nim b/lib/pure/collections/sets.nim
index b019da2a7..67e407576 100644
--- a/lib/pure/collections/sets.nim
+++ b/lib/pure/collections/sets.nim
@@ -80,6 +80,8 @@ type
     ## <#initOrderedSet,int>`_ before calling other procs on it.
     data: OrderedKeyValuePairSeq[A]
     counter, first, last: int
+  SomeSet*[A] = HashSet[A] | OrderedSet[A]
+    ## Type union representing `HashSet` or `OrderedSet`.
 
 const
   defaultInitialSize* = 64
@@ -907,8 +909,6 @@ iterator pairs*[A](s: OrderedSet[A]): tuple[a: int, b: A] =
   forAllOrderedPairs:
     yield (idx, s.data[h].key)
 
-
-
 # -----------------------------------------------------------------------
 
 
diff --git a/lib/pure/collections/tables.nim b/lib/pure/collections/tables.nim
index 6d79638c2..dc21c0539 100644
--- a/lib/pure/collections/tables.nim
+++ b/lib/pure/collections/tables.nim
@@ -1750,10 +1750,6 @@ iterator mvalues*[A, B](t: var OrderedTable[A, B]): var B =
     yield t.data[h].val
     assert(len(t) == L, "the length of the table changed while iterating over it")
 
-
-
-
-
 # ---------------------------------------------------------------------------
 # --------------------------- OrderedTableRef -------------------------------
 # ---------------------------------------------------------------------------
diff --git a/lib/pure/options.nim b/lib/pure/options.nim
index 4efe04d67..076cf3707 100644
--- a/lib/pure/options.nim
+++ b/lib/pure/options.nim
@@ -372,7 +372,6 @@ proc unsafeGet*[T](self: Option[T]): lent T {.inline.}=
   assert self.isSome
   result = self.val
 
-
 when isMainModule:
   import unittest, sequtils
 
diff --git a/lib/pure/strtabs.nim b/lib/pure/strtabs.nim
index 4c2dd1451..864db148f 100644
--- a/lib/pure/strtabs.nim
+++ b/lib/pure/strtabs.nim
@@ -89,6 +89,7 @@ const
   growthFactor = 2
   startSize = 64
 
+proc mode*(t: StringTableRef): StringTableMode {.inline.} = t.mode
 
 iterator pairs*(t: StringTableRef): tuple[key, value: string] =
   ## Iterates over every `(key, value)` pair in the table `t`.
@@ -422,25 +423,6 @@ proc `%`*(f: string, t: StringTableRef, flags: set[FormatFlag] = {}): string {.
       add(result, f[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:
   var x = {"k": "v", "11": "22", "565": "67"}.newStringTable
   assert x["k"] == "v"
diff --git a/lib/std/jsonutils.nim b/lib/std/jsonutils.nim
index 22f2a7a89..dd174303a 100644
--- a/lib/std/jsonutils.nim
+++ b/lib/std/jsonutils.nim
@@ -13,25 +13,33 @@ runnableExamples:
   let j = a.toJson
   doAssert j.jsonTo(type(a)).toJson == j
 
-import std/[json,tables,strutils]
+import std/[json,strutils,tables,sets,strtabs,options]
 
 #[
-xxx
-use toJsonHook,fromJsonHook for Table|OrderedTable
-add Options support also using toJsonHook,fromJsonHook and remove `json=>options` dependency
-
 Future directions:
 add a way to customize serialization, for eg:
-* allowing missing or extra fields in JsonNode
 * field renaming
 * allow serializing `enum` and `char` as `string` instead of `int`
   (enum is more compact/efficient, and robust to enum renamings, but string
   is more human readable)
 * handle cyclic references, using a cache of already visited addresses
+* implement support for serialization and de-serialization of nested variant
+  objects.
 ]#
 
 import std/macros
 
+type
+  Joptions* = object
+    ## Options controlling the behavior of `fromJson`.
+    allowExtraKeys*: bool
+      ## If `true` Nim's object to which the JSON is parsed is not required to
+      ## have a field for every JSON key.
+    allowMissingKeys*: bool
+      ## If `true` Nim's object to which JSON is parsed is allowed to have
+      ## fields without corresponding JSON keys.
+    # in future work: a key rename could be added
+
 proc isNamedTuple(T: typedesc): bool {.magic: "TypeTrait".}
 proc distinctBase(T: typedesc): typedesc {.magic: "TypeTrait".}
 template distinctBase[T](a: T): untyped = distinctBase(type(a))(a)
@@ -58,11 +66,11 @@ macro getDiscriminants(a: typedesc): seq[string] =
     result = quote do:
       seq[string].default
 
-macro initCaseObject(a: typedesc, fun: untyped): untyped =
+macro initCaseObject(T: typedesc, fun: untyped): untyped =
   ## does the minimum to construct a valid case object, only initializing
   ## the discriminant fields; see also `getDiscriminants`
   # maybe candidate for std/typetraits
-  var a = a.getTypeImpl
+  var a = T.getTypeImpl
   doAssert a.kind == nnkBracketExpr
   let sym = a[1]
   let t = sym.getTypeImpl
@@ -92,20 +100,81 @@ proc checkJsonImpl(cond: bool, condStr: string, msg = "") =
 template checkJson(cond: untyped, msg = "") =
   checkJsonImpl(cond, astToStr(cond), msg)
 
-template fromJsonFields(a, b, T, keys) =
-  checkJson b.kind == JObject, $(b.kind) # we could customize whether to allow JNull
-  var num = 0
-  for key, val in fieldPairs(a):
+proc hasField[T](obj: T, field: string): bool =
+  for k, _ in fieldPairs(obj):
+    if k == field:
+      return true
+  return false
+
+macro accessField(obj: typed, name: static string): untyped = 
+  newDotExpr(obj, ident(name))
+
+template fromJsonFields(newObj, oldObj, json, discKeys, opt) =
+  type T = typeof(newObj)
+  # we could customize whether to allow JNull
+  checkJson json.kind == JObject, $json.kind
+  var num, numMatched = 0
+  for key, val in fieldPairs(newObj):
     num.inc
-    when key notin keys:
-      if b.hasKey key:
-        fromJson(val, b[key])
+    when key notin discKeys:
+      if json.hasKey key:
+        numMatched.inc
+        fromJson(val, json[key])
+      elif opt.allowMissingKeys:
+        # if there are no discriminant keys the `oldObj` must always have the
+        # same keys as the new one. Otherwise we must check, because they could
+        # be set to different branches.
+        when typeof(oldObj) isnot typeof(nil):
+          if discKeys.len == 0 or hasField(oldObj, key):
+            val = accessField(oldObj, key)
       else:
-        # we could customize to allow this
-        checkJson false, $($T, key, b)
-  checkJson b.len == num, $(b.len, num, $T, b) # could customize
+        checkJson false, $($T, key, json)
+    else:
+      if json.hasKey key:
+        numMatched.inc
+
+  let ok =
+    if opt.allowExtraKeys and opt.allowMissingKeys:
+      true
+    elif opt.allowExtraKeys:
+      # This check is redundant because if here missing keys are not allowed,
+      # and if `num != numMatched` it will fail in the loop above but it is left
+      # for clarity.
+      assert num == numMatched
+      num == numMatched
+    elif opt.allowMissingKeys:
+      json.len == numMatched
+    else:
+      json.len == num and num == numMatched
+  
+  checkJson ok, $(json.len, num, numMatched, $T, json)
 
-proc fromJson*[T](a: var T, b: JsonNode) =
+proc fromJson*[T](a: var T, b: JsonNode, opt = Joptions())
+
+proc discKeyMatch[T](obj: T, json: JsonNode, key: static string): bool =
+  if not json.hasKey key:
+    return true
+  let field = accessField(obj, key)
+  var jsonVal: typeof(field)
+  fromJson(jsonVal, json[key])
+  if jsonVal != field:
+    return false
+  return true
+
+macro discKeysMatchBodyGen(obj: typed, json: JsonNode,
+                           keys: static seq[string]): untyped =
+  result = newStmtList()
+  let r = ident("result")
+  for key in keys:
+    let keyLit = newLit key
+    result.add quote do:
+      `r` = `r` and discKeyMatch(`obj`, `json`, `keyLit`)
+
+proc discKeysMatch[T](obj: T, json: JsonNode, keys: static seq[string]): bool =
+  result = true
+  discKeysMatchBodyGen(obj, json, keys)
+
+proc fromJson*[T](a: var T, b: JsonNode, opt = Joptions()) =
   ## inplace version of `jsonTo`
   #[
   adding "json path" leading to `b` can be added in future work.
@@ -113,10 +182,6 @@ proc fromJson*[T](a: var T, b: JsonNode) =
   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())
@@ -148,14 +213,26 @@ proc fromJson*[T](a: var T, b: JsonNode) =
     for i, val in b.getElems:
       fromJson(a[i], val)
   elif T is object:
-    template fun(key, typ): untyped =
-      jsonTo(b[key], typ)
-    a = initCaseObject(T, fun)
+    template fun(key, typ): untyped {.used.} =
+      if b.hasKey key:
+        jsonTo(b[key], typ)
+      elif hasField(a, key):
+        accessField(a, key)
+      else:
+        default(typ)
     const keys = getDiscriminants(T)
-    fromJsonFields(a, b, T, keys)
+    when keys.len == 0:
+      fromJsonFields(a, nil, b, keys, opt)
+    else:
+      if discKeysMatch(a, b, keys):
+        fromJsonFields(a, nil, b, keys, opt)
+      else:
+        var newObj = initCaseObject(T, fun)
+        fromJsonFields(newObj, a, b, keys, opt)
+        a = newObj
   elif T is tuple:
     when isNamedTuple(T):
-      fromJsonFields(a, b, T, seq[string].default)
+      fromJsonFields(a, nil, b, seq[string].default, opt)
     else:
       checkJson b.kind == JArray, $(b.kind) # we could customize whether to allow JNull
       var i = 0
@@ -175,9 +252,6 @@ 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:
     when T is object or isNamedTuple(T):
       result = newJObject()
@@ -198,3 +272,145 @@ proc toJson*[T](a: T): JsonNode =
   elif T is bool: result = %(a)
   elif T is Ordinal: result = %(a.ord)
   else: result = %a
+
+proc fromJsonHook*[K, V](t: var (Table[K, V] | OrderedTable[K, V]),
+                         jsonNode: JsonNode) =
+  ## Enables `fromJson` for `Table` and `OrderedTable` types.
+  ## 
+  ## See also:
+  ## * `toJsonHook proc<#toJsonHook,(Table[K,V]|OrderedTable[K,V])>`_
+  runnableExamples:
+    import tables, json
+    var foo: tuple[t: Table[string, int], ot: OrderedTable[string, int]]
+    fromJson(foo, parseJson("""
+      {"t":{"two":2,"one":1},"ot":{"one":1,"three":3}}"""))
+    assert foo.t == [("one", 1), ("two", 2)].toTable
+    assert foo.ot == [("one", 1), ("three", 3)].toOrderedTable
+
+  assert jsonNode.kind == JObject,
+          "The kind of the `jsonNode` must be `JObject`, but its actual " &
+          "type is `" & $jsonNode.kind & "`."
+  clear(t)
+  for k, v in jsonNode:
+    t[k] = jsonTo(v, V)
+
+proc toJsonHook*[K, V](t: (Table[K, V] | OrderedTable[K, V])): JsonNode =
+  ## Enables `toJson` for `Table` and `OrderedTable` types.
+  ##
+  ## See also:
+  ## * `fromJsonHook proc<#fromJsonHook,(Table[K,V]|OrderedTable[K,V]),JsonNode>`_
+  runnableExamples:
+    import tables, json
+    let foo = (
+      t: [("two", 2)].toTable,
+      ot: [("one", 1), ("three", 3)].toOrderedTable)
+    assert $toJson(foo) == """{"t":{"two":2},"ot":{"one":1,"three":3}}"""
+
+  result = newJObject()
+  for k, v in pairs(t):
+    result[k] = toJson(v)
+
+proc fromJsonHook*[A](s: var SomeSet[A], jsonNode: JsonNode) =
+  ## Enables `fromJson` for `HashSet` and `OrderedSet` types.
+  ## 
+  ## See also:
+  ## * `toJsonHook proc<#toJsonHook,SomeSet[A]>`_
+  runnableExamples:
+    import sets, json
+    var foo: tuple[hs: HashSet[string], os: OrderedSet[string]]
+    fromJson(foo, parseJson("""
+      {"hs": ["hash", "set"], "os": ["ordered", "set"]}"""))
+    assert foo.hs == ["hash", "set"].toHashSet
+    assert foo.os == ["ordered", "set"].toOrderedSet
+
+  assert jsonNode.kind == JArray,
+          "The kind of the `jsonNode` must be `JArray`, but its actual " &
+          "type is `" & $jsonNode.kind & "`."
+  clear(s)
+  for v in jsonNode:
+    incl(s, jsonTo(v, A))
+
+proc toJsonHook*[A](s: SomeSet[A]): JsonNode =
+  ## Enables `toJson` for `HashSet` and `OrderedSet` types.
+  ##
+  ## See also:
+  ## * `fromJsonHook proc<#fromJsonHook,SomeSet[A],JsonNode>`_
+  runnableExamples:
+    import sets, json
+    let foo = (hs: ["hash"].toHashSet, os: ["ordered", "set"].toOrderedSet)
+    assert $toJson(foo) == """{"hs":["hash"],"os":["ordered","set"]}"""
+
+  result = newJArray()
+  for k in s:
+    add(result, toJson(k))
+
+proc fromJsonHook*[T](self: var Option[T], jsonNode: JsonNode) =
+  ## Enables `fromJson` for `Option` types.
+  ## 
+  ## See also:
+  ## * `toJsonHook proc<#toJsonHook,Option[T]>`_
+  runnableExamples:
+    import options, json
+    var opt: Option[string]
+    fromJsonHook(opt, parseJson("\"test\""))
+    assert get(opt) == "test"
+    fromJson(opt, parseJson("null"))
+    assert isNone(opt)
+
+  if jsonNode.kind != JNull:
+    self = some(jsonTo(jsonNode, T))
+  else:
+    self = none[T]()
+
+proc toJsonHook*[T](self: Option[T]): JsonNode =
+  ## Enables `toJson` for `Option` types.
+  ##
+  ## See also:
+  ## * `fromJsonHook proc<#fromJsonHook,Option[T],JsonNode>`_
+  runnableExamples:
+    import options, json
+    let optSome = some("test")
+    assert $toJson(optSome) == "\"test\""
+    let optNone = none[string]()
+    assert $toJson(optNone) == "null"
+
+  if isSome(self):
+    toJson(get(self))
+  else:
+    newJNull()
+
+proc fromJsonHook*(a: var StringTableRef, b: JsonNode) =
+  ## Enables `fromJson` for `StringTableRef` type.
+  ## 
+  ## See also:
+  ## * `toJsonHook` proc<#toJsonHook,StringTableRef>`_
+  runnableExamples:
+    import strtabs, json
+    var t = newStringTable(modeCaseSensitive)
+    let jsonStr = """{"mode": 0, "table": {"name": "John", "surname": "Doe"}}"""
+    fromJsonHook(t, parseJson(jsonStr))
+    assert t[] == newStringTable("name", "John", "surname", "Doe",
+                                 modeCaseSensitive)[]
+
+  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): JsonNode =
+  ## Enables `toJson` for `StringTableRef` type.
+  ## 
+  ## See also:
+  ## * `fromJsonHook` proc<#fromJsonHook,StringTableRef,JsonNode>`_
+  runnableExamples:
+    import strtabs, json
+    let t = newStringTable("name", "John", "surname", "Doe", modeCaseSensitive)
+    let jsonStr = """{"mode": "modeCaseSensitive",
+                      "table": {"name": "John", "surname": "Doe"}}"""
+    assert toJson(t) == parseJson(jsonStr)
+
+  result = newJObject()
+  result["mode"] = toJson($a.mode)
+  let t = newJObject()
+  for k,v in a: t[k] = toJson(v)
+  result["table"] = t
diff --git a/tests/stdlib/tjsonutils.nim b/tests/stdlib/tjsonutils.nim
index 0b2ec7179..fefd412e7 100644
--- a/tests/stdlib/tjsonutils.nim
+++ b/tests/stdlib/tjsonutils.nim
@@ -13,8 +13,7 @@ proc testRoundtrip[T](t: T, expected: string) =
   t2.fromJson(j)
   doAssert t2.toJson == j
 
-import tables
-import strtabs
+import tables, sets, algorithm, sequtils, options, strtabs
 
 type Foo = ref object
   id: int
@@ -119,5 +118,187 @@ template fn() =
     testRoundtrip(Foo[int](t1: false, z2: 7)): """{"t1":false,"z2":7}"""
     # pending https://github.com/nim-lang/Nim/issues/14698, test with `type Foo[T] = ref object`
 
+  block testHashSet:
+    testRoundtrip(HashSet[string]()): "[]"
+    testRoundtrip([""].toHashSet): """[""]"""
+    testRoundtrip(["one"].toHashSet): """["one"]"""
+
+    var s: HashSet[string]
+    fromJson(s, parseJson("""["one","two"]"""))
+    doAssert s == ["one", "two"].toHashSet
+
+    let jsonNode = toJson(s)
+    doAssert jsonNode.elems.mapIt(it.str).sorted == @["one", "two"]
+
+  block testOrderedSet:
+    testRoundtrip(["one", "two", "three"].toOrderedSet):
+      """["one","two","three"]"""
+
+  block testOption:
+    testRoundtrip(some("test")): "\"test\""
+    testRoundtrip(none[string]()): "null"
+    testRoundtrip(some(42)): "42"
+    testRoundtrip(none[int]()): "null"
+
+  block testStrtabs:
+    testRoundtrip(newStringTable(modeStyleInsensitive)):
+      """{"mode":"modeStyleInsensitive","table":{}}"""
+
+    testRoundtrip(
+      newStringTable("name", "John", "surname", "Doe", modeCaseSensitive)):
+        """{"mode":"modeCaseSensitive","table":{"name":"John","surname":"Doe"}}"""
+
+  block testJoptions:
+    type
+      AboutLifeUniverseAndEverythingElse = object
+        question: string
+        answer: int
+
+    block testExceptionOnExtraKeys:
+      var guide: AboutLifeUniverseAndEverythingElse
+      let json = parseJson(
+        """{"question":"6*9=?","answer":42,"author":"Douglas Adams"}""")
+      doAssertRaises ValueError, fromJson(guide, json)
+      doAssertRaises ValueError,
+                     fromJson(guide, json, Joptions(allowMissingKeys: true))
+
+      type
+        A = object
+          a1,a2,a3: int
+      var a: A
+      let j = parseJson("""{"a3": 1, "a4": 2}""")
+      doAssertRaises ValueError,
+                     fromJson(a, j, Joptions(allowMissingKeys: true))
+
+    block testExceptionOnMissingKeys:
+      var guide: AboutLifeUniverseAndEverythingElse
+      let json = parseJson("""{"answer":42}""")
+      doAssertRaises ValueError, fromJson(guide, json)
+      doAssertRaises ValueError,
+                     fromJson(guide, json, Joptions(allowExtraKeys: true))
+
+    block testAllowExtraKeys:
+      var guide: AboutLifeUniverseAndEverythingElse
+      let json = parseJson(
+        """{"question":"6*9=?","answer":42,"author":"Douglas Adams"}""")
+      fromJson(guide, json, Joptions(allowExtraKeys: true))
+      doAssert guide == AboutLifeUniverseAndEverythingElse(
+        question: "6*9=?", answer: 42)
+
+    block testAllowMissingKeys:
+      var guide = AboutLifeUniverseAndEverythingElse(
+        question: "6*9=?", answer: 54)
+      let json = parseJson("""{"answer":42}""")
+      fromJson(guide, json, Joptions(allowMissingKeys: true))
+      doAssert guide == AboutLifeUniverseAndEverythingElse(
+        question: "6*9=?", answer: 42)
+
+    block testAllowExtraAndMissingKeys:
+      var guide = AboutLifeUniverseAndEverythingElse(
+        question: "6*9=?", answer: 54)
+      let json = parseJson(
+        """{"answer":42,"author":"Douglas Adams"}""")
+      fromJson(guide, json, Joptions(
+        allowExtraKeys: true, allowMissingKeys: true))
+      doAssert guide == AboutLifeUniverseAndEverythingElse(
+        question: "6*9=?", answer: 42)
+
+    type
+      Foo = object
+        a: array[2, string]
+        case b: bool
+        of false: f: float
+        of true: t: tuple[i: int, s: string]
+        case c: range[0 .. 2]
+        of 0: c0: int
+        of 1: c1: float
+        of 2: c2: string
+
+    block testExceptionOnMissingDiscriminantKey:
+      var foo: Foo
+      let json = parseJson("""{"a":["one","two"]}""")
+      doAssertRaises ValueError, fromJson(foo, json)
+
+    block testDoNotResetMissingFieldsWhenHaveDiscriminantKey:
+      var foo = Foo(a: ["one", "two"], b: true, t: (i: 42, s: "s"),
+                    c: 0, c0: 1)
+      let json = parseJson("""{"b":true,"c":2}""")
+      fromJson(foo, json, Joptions(allowMissingKeys: true))
+      doAssert foo.a == ["one", "two"]
+      doAssert foo.b
+      doAssert foo.t == (i: 42, s: "s")
+      doAssert foo.c == 2
+      doAssert foo.c2 == ""
+
+    block testAllowMissingDiscriminantKeys:
+      var foo: Foo
+      let json = parseJson("""{"a":["one","two"],"c":1,"c1":3.14159}""")
+      fromJson(foo, json, Joptions(allowMissingKeys: true))
+      doAssert foo.a == ["one", "two"]
+      doAssert not foo.b
+      doAssert foo.f == 0.0
+      doAssert foo.c == 1
+      doAssert foo.c1 == 3.14159
+
+    block testExceptionOnWrongDiscirminatBranchInJson:
+      var foo = Foo(b: false, f: 3.14159, c: 0, c0: 42)
+      let json = parseJson("""{"c2": "hello"}""")
+      doAssertRaises ValueError,
+                     fromJson(foo, json, Joptions(allowMissingKeys: true))
+      # Test that the original fields are not reset.
+      doAssert not foo.b
+      doAssert foo.f == 3.14159
+      doAssert foo.c == 0
+      doAssert foo.c0 == 42
+
+    block testNoExceptionOnRightDiscriminantBranchInJson:
+      var foo = Foo(b: false, f: 0, c:1, c1: 0)
+      let json = parseJson("""{"f":2.71828,"c1": 3.14159}""")
+      fromJson(foo, json, Joptions(allowMissingKeys: true))
+      doAssert not foo.b
+      doAssert foo.f == 2.71828
+      doAssert foo.c == 1
+      doAssert foo.c1 == 3.14159
+
+    block testAllowExtraKeysInJsonOnWrongDisciriminatBranch:
+      var foo = Foo(b: false, f: 3.14159, c: 0, c0: 42)
+      let json = parseJson("""{"c2": "hello"}""")
+      fromJson(foo, json, Joptions(allowMissingKeys: true,
+                                   allowExtraKeys: true))
+      # Test that the original fields are not reset.
+      doAssert not foo.b
+      doAssert foo.f == 3.14159
+      doAssert foo.c == 0
+      doAssert foo.c0 == 42
+
+    when false:
+      ## TODO: Implement support for nested variant objects allowing the tests
+      ## bellow to pass.
+      block testNestedVariantObjects:
+        type
+          Variant = object
+            case b: bool
+            of false:
+              case bf: bool
+              of false: bff: int
+              of true: bft: float
+            of true:
+              case bt: bool
+              of false: btf: string
+              of true: btt: char
+
+        testRoundtrip(Variant(b: false, bf: false, bff: 42)):
+          """{"b": false, "bf": false, "bff": 42}"""
+        testRoundtrip(Variant(b: false, bf: true, bft: 3.14159)):
+          """{"b": false, "bf": true, "bft": 3.14159}"""
+        testRoundtrip(Variant(b: true, bt: false, btf: "test")):
+          """{"b": true, "bt": false, "btf": "test"}"""
+        testRoundtrip(Variant(b: true, bt: true, btt: 'c')):
+          """{"b": true, "bt": true, "btt": "c"}"""
+        
+        # TODO: Add additional tests with missing and extra JSON keys, both when
+        # allowed and forbidden analogous to the tests for the not nested
+        # variant objects.
+
 static: fn()
 fn()