Files
Nim/lib/pure/json.nim
ASVIEST 20d79c9fb0 Deprecate asm stmt for js target (#23149)
why ?

- We already have an emit that does the same thing
- The name asm itself is a bit confusing, you might think it's an alias
for asm.js or something else.
- The asm keyword is used differently on different compiler targets (it
makes it inexpressive).
- Does anyone (other than some compiler libraries) use asm instead of
emit ? If yes, it's a bit strange to use asm somewhere and emit
somewhere. By making the asm keyword for js target deprecated, there
would be even less use of the asm keyword for js target, reducing the
amount of confusion.
- New users might accidentally use a non-universal approach via the asm
keyword instead of emit, and then when they learn about asm, try to
figure out what the differences are.

see https://forum.nim-lang.org/t/10821

---------

Co-authored-by: Andreas Rumpf <rumpf_a@web.de>
2024-01-02 07:49:54 +01:00

1395 lines
45 KiB
Nim

#
#
# Nim's Runtime Library
# (c) Copyright 2015 Andreas Rumpf, Dominik Picheta
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## This module implements a simple high performance `JSON`:idx:
## parser. JSON (JavaScript Object Notation) is a lightweight
## data-interchange format that is easy for humans to read and write
## (unlike XML). It is easy for machines to parse and generate.
## JSON is based on a subset of the JavaScript Programming Language,
## Standard ECMA-262 3rd Edition - December 1999.
##
## See also
## ========
## * `std/parsejson <parsejson.html>`_
## * `std/jsonutils <jsonutils.html>`_
## * `std/marshal <marshal.html>`_
## * `std/jscore <jscore.html>`_
##
##
## Overview
## ========
##
## Parsing JSON
## ------------
##
## JSON often arrives into your program (via an API or a file) as a `string`.
## The first step is to change it from its serialized form into a nested object
## structure called a `JsonNode`.
##
## The `parseJson` procedure takes a string containing JSON and returns a
## `JsonNode` object. This is an object variant and it is either a
## `JObject`, `JArray`, `JString`, `JInt`, `JFloat`, `JBool` or
## `JNull`. You check the kind of this object variant by using the `kind`
## accessor.
##
## For a `JsonNode` who's kind is `JObject`, you can access its fields using
## the `[]` operator. The following example shows how to do this:
##
## ```Nim
## import std/json
##
## let jsonNode = parseJson("""{"key": 3.14}""")
##
## doAssert jsonNode.kind == JObject
## doAssert jsonNode["key"].kind == JFloat
## ```
##
## Reading values
## --------------
##
## Once you have a `JsonNode`, retrieving the values can then be achieved
## by using one of the helper procedures, which include:
##
## * `getInt`
## * `getFloat`
## * `getStr`
## * `getBool`
##
## To retrieve the value of `"key"` you can do the following:
##
## ```Nim
## import std/json
##
## let jsonNode = parseJson("""{"key": 3.14}""")
##
## doAssert jsonNode["key"].getFloat() == 3.14
## ```
##
## **Important:** The `[]` operator will raise an exception when the
## specified field does not exist.
##
## Handling optional keys
## ----------------------
##
## By using the `{}` operator instead of `[]`, it will return `nil`
## when the field is not found. The `get`-family of procedures will return a
## type's default value when called on `nil`.
##
## ```Nim
## import std/json
##
## let jsonNode = parseJson("{}")
##
## doAssert jsonNode{"nope"}.getInt() == 0
## doAssert jsonNode{"nope"}.getFloat() == 0
## doAssert jsonNode{"nope"}.getStr() == ""
## doAssert jsonNode{"nope"}.getBool() == false
## ```
##
## Using default values
## --------------------
##
## The `get`-family helpers also accept an additional parameter which allow
## you to fallback to a default value should the key's values be `null`:
##
## ```Nim
## import std/json
##
## let jsonNode = parseJson("""{"key": 3.14, "key2": null}""")
##
## doAssert jsonNode["key"].getFloat(6.28) == 3.14
## doAssert jsonNode["key2"].getFloat(3.14) == 3.14
## doAssert jsonNode{"nope"}.getFloat(3.14) == 3.14 # note the {}
## ```
##
## Unmarshalling
## -------------
##
## In addition to reading dynamic data, Nim can also unmarshal JSON directly
## into a type with the `to` macro.
##
## Note: Use `Option <options.html#Option>`_ for keys sometimes missing in json
## responses, and backticks around keys with a reserved keyword as name.
##
## ```Nim
## import std/json
## import std/options
##
## type
## User = object
## name: string
## age: int
## `type`: Option[string]
##
## let userJson = parseJson("""{ "name": "Nim", "age": 12 }""")
## let user = to(userJson, User)
## if user.`type`.isSome():
## assert user.`type`.get() != "robot"
## ```
##
## Creating JSON
## =============
##
## This module can also be used to comfortably create JSON using the `%*`
## operator:
##
## ```nim
## import std/json
##
## var hisName = "John"
## let herAge = 31
## var j = %*
## [
## { "name": hisName, "age": 30 },
## { "name": "Susan", "age": herAge }
## ]
##
## var j2 = %* {"name": "Isaac", "books": ["Robot Dreams"]}
## j2["details"] = %* {"age":35, "pi":3.1415}
## echo j2
## ```
##
## See also: std/jsonutils for hookable json serialization/deserialization
## of arbitrary types.
runnableExamples:
## Note: for JObject, key ordering is preserved, unlike in some languages,
## this is convenient for some use cases. Example:
type Foo = object
a1, a2, a0, a3, a4: int
doAssert $(%* Foo()) == """{"a1":0,"a2":0,"a0":0,"a3":0,"a4":0}"""
import std/[hashes, tables, strutils, lexbase, streams, macros, parsejson]
import std/options # xxx remove this dependency using same approach as https://github.com/nim-lang/Nim/pull/14563
import std/private/since
when defined(nimPreviewSlimSystem):
import std/[syncio, assertions, formatfloat]
export
tables.`$`
export
parsejson.JsonEventKind, parsejson.JsonError, JsonParser, JsonKindError,
open, close, str, getInt, getFloat, kind, getColumn, getLine, getFilename,
errorMsg, errorMsgExpected, next, JsonParsingError, raiseParseErr, nimIdentNormalize
type
JsonNodeKind* = enum ## possible JSON node types
JNull,
JBool,
JInt,
JFloat,
JString,
JObject,
JArray
JsonNode* = ref JsonNodeObj ## JSON node
JsonNodeObj* {.acyclic.} = object
isUnquoted: bool # the JString was a number-like token and
# so shouldn't be quoted
case kind*: JsonNodeKind
of JString:
str*: string
of JInt:
num*: BiggestInt
of JFloat:
fnum*: float
of JBool:
bval*: bool
of JNull:
nil
of JObject:
fields*: OrderedTable[string, JsonNode]
of JArray:
elems*: seq[JsonNode]
const DepthLimit = 1000
proc newJString*(s: string): JsonNode =
## Creates a new `JString JsonNode`.
result = JsonNode(kind: JString, str: s)
proc newJRawNumber(s: string): JsonNode =
## Creates a "raw JS number", that is a number that does not
## fit into Nim's `BiggestInt` field. This is really a `JString`
## with the additional information that it should be converted back
## to the string representation without the quotes.
result = JsonNode(kind: JString, str: s, isUnquoted: true)
proc newJInt*(n: BiggestInt): JsonNode =
## Creates a new `JInt JsonNode`.
result = JsonNode(kind: JInt, num: n)
proc newJFloat*(n: float): JsonNode =
## Creates a new `JFloat JsonNode`.
result = JsonNode(kind: JFloat, fnum: n)
proc newJBool*(b: bool): JsonNode =
## Creates a new `JBool JsonNode`.
result = JsonNode(kind: JBool, bval: b)
proc newJNull*(): JsonNode =
## Creates a new `JNull JsonNode`.
result = JsonNode(kind: JNull)
proc newJObject*(): JsonNode =
## Creates a new `JObject JsonNode`
result = JsonNode(kind: JObject, fields: initOrderedTable[string, JsonNode](2))
proc newJArray*(): JsonNode =
## Creates a new `JArray JsonNode`
result = JsonNode(kind: JArray, elems: @[])
proc getStr*(n: JsonNode, default: string = ""): string =
## Retrieves the string value of a `JString JsonNode`.
##
## Returns `default` if `n` is not a `JString`, or if `n` is nil.
if n.isNil or n.kind != JString: return default
else: return n.str
proc getInt*(n: JsonNode, default: int = 0): int =
## Retrieves the int value of a `JInt JsonNode`.
##
## Returns `default` if `n` is not a `JInt`, or if `n` is nil.
if n.isNil or n.kind != JInt: return default
else: return int(n.num)
proc getBiggestInt*(n: JsonNode, default: BiggestInt = 0): BiggestInt =
## Retrieves the BiggestInt value of a `JInt JsonNode`.
##
## Returns `default` if `n` is not a `JInt`, or if `n` is nil.
if n.isNil or n.kind != JInt: return default
else: return n.num
proc getFloat*(n: JsonNode, default: float = 0.0): float =
## Retrieves the float value of a `JFloat JsonNode`.
##
## Returns `default` if `n` is not a `JFloat` or `JInt`, or if `n` is nil.
if n.isNil: return default
case n.kind
of JFloat: return n.fnum
of JInt: return float(n.num)
else: return default
proc getBool*(n: JsonNode, default: bool = false): bool =
## Retrieves the bool value of a `JBool JsonNode`.
##
## Returns `default` if `n` is not a `JBool`, or if `n` is nil.
if n.isNil or n.kind != JBool: return default
else: return n.bval
proc getFields*(n: JsonNode,
default = initOrderedTable[string, JsonNode](2)):
OrderedTable[string, JsonNode] =
## Retrieves the key, value pairs of a `JObject JsonNode`.
##
## Returns `default` if `n` is not a `JObject`, or if `n` is nil.
if n.isNil or n.kind != JObject: return default
else: return n.fields
proc getElems*(n: JsonNode, default: seq[JsonNode] = @[]): seq[JsonNode] =
## Retrieves the array of a `JArray JsonNode`.
##
## Returns `default` if `n` is not a `JArray`, or if `n` is nil.
if n.isNil or n.kind != JArray: return default
else: return n.elems
proc add*(father, child: JsonNode) =
## Adds `child` to a JArray node `father`.
assert father.kind == JArray
father.elems.add(child)
proc add*(obj: JsonNode, key: string, val: JsonNode) =
## Sets a field from a `JObject`.
assert obj.kind == JObject
obj.fields[key] = val
proc `%`*(s: string): JsonNode =
## Generic constructor for JSON data. Creates a new `JString JsonNode`.
result = JsonNode(kind: JString, str: s)
proc `%`*(n: uint): JsonNode =
## Generic constructor for JSON data. Creates a new `JInt JsonNode`.
if n > cast[uint](int.high):
result = newJRawNumber($n)
else:
result = JsonNode(kind: JInt, num: BiggestInt(n))
proc `%`*(n: int): JsonNode =
## Generic constructor for JSON data. Creates a new `JInt JsonNode`.
result = JsonNode(kind: JInt, num: n)
proc `%`*(n: BiggestUInt): JsonNode =
## Generic constructor for JSON data. Creates a new `JInt JsonNode`.
if n > cast[BiggestUInt](BiggestInt.high):
result = newJRawNumber($n)
else:
result = JsonNode(kind: JInt, num: BiggestInt(n))
proc `%`*(n: BiggestInt): JsonNode =
## Generic constructor for JSON data. Creates a new `JInt JsonNode`.
result = JsonNode(kind: JInt, num: n)
proc `%`*(n: float): JsonNode =
## Generic constructor for JSON data. Creates a new `JFloat JsonNode`.
runnableExamples:
assert $(%[NaN, Inf, -Inf, 0.0, -0.0, 1.0, 1e-2]) == """["nan","inf","-inf",0.0,-0.0,1.0,0.01]"""
assert (%NaN).kind == JString
assert (%0.0).kind == JFloat
# for those special cases, we could also have used `newJRawNumber` but then
# it would've been inconsisten with the case of `parseJson` vs `%` for representing them.
if n != n: newJString("nan")
elif n == Inf: newJString("inf")
elif n == -Inf: newJString("-inf")
else: JsonNode(kind: JFloat, fnum: n)
proc `%`*(b: bool): JsonNode =
## Generic constructor for JSON data. Creates a new `JBool JsonNode`.
result = JsonNode(kind: JBool, bval: b)
proc `%`*(keyVals: openArray[tuple[key: string, val: JsonNode]]): JsonNode =
## Generic constructor for JSON data. Creates a new `JObject JsonNode`
if keyVals.len == 0: return newJArray()
result = newJObject()
for key, val in items(keyVals): result.fields[key] = val
template `%`*(j: JsonNode): JsonNode = j
proc `%`*[T](elements: openArray[T]): JsonNode =
## Generic constructor for JSON data. Creates a new `JArray JsonNode`
result = newJArray()
for elem in elements: result.add(%elem)
proc `%`*[T](table: Table[string, T]|OrderedTable[string, T]): JsonNode =
## Generic constructor for JSON data. Creates a new `JObject JsonNode`.
result = newJObject()
for k, v in table: result[k] = %v
proc `%`*[T](opt: Option[T]): JsonNode =
## Generic constructor for JSON data. Creates a new `JNull JsonNode`
## if `opt` is empty, otherwise it delegates to the underlying value.
if opt.isSome: %opt.get else: newJNull()
when false:
# For 'consistency' we could do this, but that only pushes people further
# into that evil comfort zone where they can use Nim without understanding it
# causing problems later on.
proc `%`*(elements: set[bool]): JsonNode =
## Generic constructor for JSON data. Creates a new `JObject JsonNode`.
## This can only be used with the empty set `{}` and is supported
## to prevent the gotcha `%*{}` which used to produce an empty
## JSON array.
result = newJObject()
assert false notin elements, "usage error: only empty sets allowed"
assert true notin elements, "usage error: only empty sets allowed"
proc `[]=`*(obj: JsonNode, key: string, val: JsonNode) {.inline.} =
## Sets a field from a `JObject`.
assert(obj.kind == JObject)
obj.fields[key] = val
proc `%`*[T: object](o: T): JsonNode =
## Construct JsonNode from tuples and objects.
result = newJObject()
for k, v in o.fieldPairs: result[k] = %v
proc `%`*(o: ref object): JsonNode =
## Generic constructor for JSON data. Creates a new `JObject JsonNode`
if o.isNil:
result = newJNull()
else:
result = %(o[])
proc `%`*(o: enum): JsonNode =
## Construct a JsonNode that represents the specified enum value as a
## string. Creates a new `JString JsonNode`.
result = %($o)
proc toJsonImpl(x: NimNode): NimNode =
case x.kind
of nnkBracket: # array
if x.len == 0: return newCall(bindSym"newJArray")
result = newNimNode(nnkBracket)
for i in 0 ..< x.len:
result.add(toJsonImpl(x[i]))
result = newCall(bindSym("%", brOpen), result)
of nnkTableConstr: # object
if x.len == 0: return newCall(bindSym"newJObject")
result = newNimNode(nnkTableConstr)
for i in 0 ..< x.len:
x[i].expectKind nnkExprColonExpr
result.add newTree(nnkExprColonExpr, x[i][0], toJsonImpl(x[i][1]))
result = newCall(bindSym("%", brOpen), result)
of nnkCurly: # empty object
x.expectLen(0)
result = newCall(bindSym"newJObject")
of nnkNilLit:
result = newCall(bindSym"newJNull")
of nnkPar:
if x.len == 1: result = toJsonImpl(x[0])
else: result = newCall(bindSym("%", brOpen), x)
else:
result = newCall(bindSym("%", brOpen), x)
macro `%*`*(x: untyped): untyped =
## Convert an expression to a JsonNode directly, without having to specify
## `%` for every element.
result = toJsonImpl(x)
proc `==`*(a, b: JsonNode): bool {.noSideEffect, raises: [].} =
## Check two nodes for equality
if a.isNil:
if b.isNil: return true
return false
elif b.isNil or a.kind != b.kind:
return false
else:
case a.kind
of JString:
result = a.str == b.str
of JInt:
result = a.num == b.num
of JFloat:
result = a.fnum == b.fnum
of JBool:
result = a.bval == b.bval
of JNull:
result = true
of JArray:
{.cast(raises: []).}: # bug #19303
result = a.elems == b.elems
of JObject:
# we cannot use OrderedTable's equality here as
# the order does not matter for equality here.
if a.fields.len != b.fields.len: return false
for key, val in a.fields:
if not b.fields.hasKey(key): return false
{.cast(raises: []).}:
when defined(nimHasEffectsOf):
{.noSideEffect.}:
if b.fields[key] != val: return false
else:
if b.fields[key] != val: return false
result = true
proc hash*(n: OrderedTable[string, JsonNode]): Hash {.noSideEffect.}
proc hash*(n: JsonNode): Hash {.noSideEffect.} =
## Compute the hash for a JSON node
case n.kind
of JArray:
result = hash(n.elems)
of JObject:
result = hash(n.fields)
of JInt:
result = hash(n.num)
of JFloat:
result = hash(n.fnum)
of JBool:
result = hash(n.bval.int)
of JString:
result = hash(n.str)
of JNull:
result = Hash(0)
proc hash*(n: OrderedTable[string, JsonNode]): Hash =
for key, val in n:
result = result xor (hash(key) !& hash(val))
result = !$result
proc len*(n: JsonNode): int =
## If `n` is a `JArray`, it returns the number of elements.
## If `n` is a `JObject`, it returns the number of pairs.
## Else it returns 0.
case n.kind
of JArray: result = n.elems.len
of JObject: result = n.fields.len
else: discard
proc `[]`*(node: JsonNode, name: string): JsonNode {.inline.} =
## Gets a field from a `JObject`, which must not be nil.
## If the value at `name` does not exist, raises KeyError.
assert(not isNil(node))
assert(node.kind == JObject)
when defined(nimJsonGet):
if not node.fields.hasKey(name): return nil
result = node.fields[name]
proc `[]`*(node: JsonNode, index: int): JsonNode {.inline.} =
## Gets the node at `index` in an Array. Result is undefined if `index`
## is out of bounds, but as long as array bound checks are enabled it will
## result in an exception.
assert(not isNil(node))
assert(node.kind == JArray)
return node.elems[index]
proc `[]`*(node: JsonNode, index: BackwardsIndex): JsonNode {.inline, since: (1, 5, 1).} =
## Gets the node at `array.len-i` in an array through the `^` operator.
##
## i.e. `j[^i]` is a shortcut for `j[j.len-i]`.
runnableExamples:
let
j = parseJson("[1,2,3,4,5]")
doAssert j[^1].getInt == 5
doAssert j[^2].getInt == 4
`[]`(node, node.len - int(index))
proc `[]`*[U, V](a: JsonNode, x: HSlice[U, V]): JsonNode =
## Slice operation for JArray.
##
## Returns the inclusive range `[a[x.a], a[x.b]]`:
runnableExamples:
import std/json
let arr = %[0,1,2,3,4,5]
doAssert arr[2..4] == %[2,3,4]
doAssert arr[2..^2] == %[2,3,4]
doAssert arr[^4..^2] == %[2,3,4]
assert(a.kind == JArray)
result = newJArray()
let xa = (when x.a is BackwardsIndex: a.len - int(x.a) else: int(x.a))
let L = (when x.b is BackwardsIndex: a.len - int(x.b) else: int(x.b)) - xa + 1
for i in 0..<L:
result.add(a[i + xa])
proc hasKey*(node: JsonNode, key: string): bool =
## Checks if `key` exists in `node`.
assert(node.kind == JObject)
result = node.fields.hasKey(key)
proc contains*(node: JsonNode, key: string): bool =
## Checks if `key` exists in `node`.
assert(node.kind == JObject)
node.fields.hasKey(key)
proc contains*(node: JsonNode, val: JsonNode): bool =
## Checks if `val` exists in array `node`.
assert(node.kind == JArray)
find(node.elems, val) >= 0
proc `{}`*(node: JsonNode, keys: varargs[string]): JsonNode =
## Traverses the node and gets the given value. If any of the
## keys do not exist, returns `nil`. Also returns `nil` if one of the
## intermediate data structures is not an object.
##
## This proc can be used to create tree structures on the
## fly (sometimes called `autovivification`:idx:):
##
runnableExamples:
var myjson = %* {"parent": {"child": {"grandchild": 1}}}
doAssert myjson{"parent", "child", "grandchild"} == newJInt(1)
result = node
for key in keys:
if isNil(result) or result.kind != JObject:
return nil
result = result.fields.getOrDefault(key)
proc `{}`*(node: JsonNode, index: varargs[int]): JsonNode =
## Traverses the node and gets the given value. If any of the
## indexes do not exist, returns `nil`. Also returns `nil` if one of the
## intermediate data structures is not an array.
result = node
for i in index:
if isNil(result) or result.kind != JArray or i >= node.len:
return nil
result = result.elems[i]
proc getOrDefault*(node: JsonNode, key: string): JsonNode =
## Gets a field from a `node`. If `node` is nil or not an object or
## value at `key` does not exist, returns nil
if not isNil(node) and node.kind == JObject:
result = node.fields.getOrDefault(key)
proc `{}`*(node: JsonNode, key: string): JsonNode =
## Gets a field from a `node`. If `node` is nil or not an object or
## value at `key` does not exist, returns nil
node.getOrDefault(key)
proc `{}=`*(node: JsonNode, keys: varargs[string], value: JsonNode) =
## Traverses the node and tries to set the value at the given location
## to `value`. If any of the keys are missing, they are added.
var node = node
for i in 0..(keys.len-2):
if not node.hasKey(keys[i]):
node[keys[i]] = newJObject()
node = node[keys[i]]
node[keys[keys.len-1]] = value
proc delete*(obj: JsonNode, key: string) =
## Deletes `obj[key]`.
assert(obj.kind == JObject)
if not obj.fields.hasKey(key):
raise newException(KeyError, "key not in object")
obj.fields.del(key)
proc copy*(p: JsonNode): JsonNode =
## Performs a deep copy of `a`.
case p.kind
of JString:
result = newJString(p.str)
result.isUnquoted = p.isUnquoted
of JInt:
result = newJInt(p.num)
of JFloat:
result = newJFloat(p.fnum)
of JBool:
result = newJBool(p.bval)
of JNull:
result = newJNull()
of JObject:
result = newJObject()
for key, val in pairs(p.fields):
result.fields[key] = copy(val)
of JArray:
result = newJArray()
for i in items(p.elems):
result.elems.add(copy(i))
# ------------- pretty printing ----------------------------------------------
proc indent(s: var string, i: int) =
s.add(spaces(i))
proc newIndent(curr, indent: int, ml: bool): int =
if ml: return curr + indent
else: return indent
proc nl(s: var string, ml: bool) =
s.add(if ml: "\n" else: " ")
proc escapeJsonUnquoted*(s: string; result: var string) =
## Converts a string `s` to its JSON representation without quotes.
## Appends to `result`.
for c in s:
case c
of '\L': result.add("\\n")
of '\b': result.add("\\b")
of '\f': result.add("\\f")
of '\t': result.add("\\t")
of '\v': result.add("\\u000b")
of '\r': result.add("\\r")
of '"': result.add("\\\"")
of '\0'..'\7': result.add("\\u000" & $ord(c))
of '\14'..'\31': result.add("\\u00" & toHex(ord(c), 2))
of '\\': result.add("\\\\")
else: result.add(c)
proc escapeJsonUnquoted*(s: string): string =
## Converts a string `s` to its JSON representation without quotes.
result = newStringOfCap(s.len + s.len shr 3)
escapeJsonUnquoted(s, result)
proc escapeJson*(s: string; result: var string) =
## Converts a string `s` to its JSON representation with quotes.
## Appends to `result`.
result.add("\"")
escapeJsonUnquoted(s, result)
result.add("\"")
proc escapeJson*(s: string): string =
## Converts a string `s` to its JSON representation with quotes.
result = newStringOfCap(s.len + s.len shr 3)
escapeJson(s, result)
proc toUgly*(result: var string, node: JsonNode) =
## Converts `node` to its JSON Representation, without
## regard for human readability. Meant to improve `$` string
## conversion performance.
##
## JSON representation is stored in the passed `result`
##
## This provides higher efficiency than the `pretty` procedure as it
## does **not** attempt to format the resulting JSON to make it human readable.
var comma = false
case node.kind:
of JArray:
result.add "["
for child in node.elems:
if comma: result.add ","
else: comma = true
result.toUgly child
result.add "]"
of JObject:
result.add "{"
for key, value in pairs(node.fields):
if comma: result.add ","
else: comma = true
key.escapeJson(result)
result.add ":"
result.toUgly value
result.add "}"
of JString:
if node.isUnquoted:
result.add node.str
else:
escapeJson(node.str, result)
of JInt:
result.addInt(node.num)
of JFloat:
result.addFloat(node.fnum)
of JBool:
result.add(if node.bval: "true" else: "false")
of JNull:
result.add "null"
proc toPretty(result: var string, node: JsonNode, indent = 2, ml = true,
lstArr = false, currIndent = 0) =
case node.kind
of JObject:
if lstArr: result.indent(currIndent) # Indentation
if node.fields.len > 0:
result.add("{")
result.nl(ml) # New line
var i = 0
for key, val in pairs(node.fields):
if i > 0:
result.add(",")
result.nl(ml) # New Line
inc i
# Need to indent more than {
result.indent(newIndent(currIndent, indent, ml))
escapeJson(key, result)
result.add(": ")
toPretty(result, val, indent, ml, false,
newIndent(currIndent, indent, ml))
result.nl(ml)
result.indent(currIndent) # indent the same as {
result.add("}")
else:
result.add("{}")
of JString:
if lstArr: result.indent(currIndent)
toUgly(result, node)
of JInt:
if lstArr: result.indent(currIndent)
result.addInt(node.num)
of JFloat:
if lstArr: result.indent(currIndent)
result.addFloat(node.fnum)
of JBool:
if lstArr: result.indent(currIndent)
result.add(if node.bval: "true" else: "false")
of JArray:
if lstArr: result.indent(currIndent)
if len(node.elems) != 0:
result.add("[")
result.nl(ml)
for i in 0..len(node.elems)-1:
if i > 0:
result.add(",")
result.nl(ml) # New Line
toPretty(result, node.elems[i], indent, ml,
true, newIndent(currIndent, indent, ml))
result.nl(ml)
result.indent(currIndent)
result.add("]")
else: result.add("[]")
of JNull:
if lstArr: result.indent(currIndent)
result.add("null")
proc pretty*(node: JsonNode, indent = 2): string =
## Returns a JSON Representation of `node`, with indentation and
## on multiple lines.
##
## Similar to prettyprint in Python.
runnableExamples:
let j = %* {"name": "Isaac", "books": ["Robot Dreams"],
"details": {"age": 35, "pi": 3.1415}}
doAssert pretty(j) == """
{
"name": "Isaac",
"books": [
"Robot Dreams"
],
"details": {
"age": 35,
"pi": 3.1415
}
}"""
result = ""
toPretty(result, node, indent)
proc `$`*(node: JsonNode): string =
## Converts `node` to its JSON Representation on one line.
result = newStringOfCap(node.len shl 1)
toUgly(result, node)
iterator items*(node: JsonNode): JsonNode =
## Iterator for the items of `node`. `node` has to be a JArray.
assert node.kind == JArray, ": items() can not iterate a JsonNode of kind " & $node.kind
for i in items(node.elems):
yield i
iterator mitems*(node: var JsonNode): var JsonNode =
## Iterator for the items of `node`. `node` has to be a JArray. Items can be
## modified.
assert node.kind == JArray, ": mitems() can not iterate a JsonNode of kind " & $node.kind
for i in mitems(node.elems):
yield i
iterator pairs*(node: JsonNode): tuple[key: string, val: JsonNode] =
## Iterator for the child elements of `node`. `node` has to be a JObject.
assert node.kind == JObject, ": pairs() can not iterate a JsonNode of kind " & $node.kind
for key, val in pairs(node.fields):
yield (key, val)
iterator keys*(node: JsonNode): string =
## Iterator for the keys in `node`. `node` has to be a JObject.
assert node.kind == JObject, ": keys() can not iterate a JsonNode of kind " & $node.kind
for key in node.fields.keys:
yield key
iterator mpairs*(node: var JsonNode): tuple[key: string, val: var JsonNode] =
## Iterator for the child elements of `node`. `node` has to be a JObject.
## Values can be modified
assert node.kind == JObject, ": mpairs() can not iterate a JsonNode of kind " & $node.kind
for key, val in mpairs(node.fields):
yield (key, val)
proc parseJson(p: var JsonParser; rawIntegers, rawFloats: bool, depth = 0): JsonNode =
## Parses JSON from a JSON Parser `p`.
case p.tok
of tkString:
# we capture 'p.a' here, so we need to give it a fresh buffer afterwards:
when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc):
result = JsonNode(kind: JString, str: move p.a)
else:
result = JsonNode(kind: JString)
shallowCopy(result.str, p.a)
p.a = ""
discard getTok(p)
of tkInt:
if rawIntegers:
result = newJRawNumber(p.a)
else:
try:
result = newJInt(parseBiggestInt(p.a))
except ValueError:
result = newJRawNumber(p.a)
discard getTok(p)
of tkFloat:
if rawFloats:
result = newJRawNumber(p.a)
else:
try:
result = newJFloat(parseFloat(p.a))
except ValueError:
result = newJRawNumber(p.a)
discard getTok(p)
of tkTrue:
result = newJBool(true)
discard getTok(p)
of tkFalse:
result = newJBool(false)
discard getTok(p)
of tkNull:
result = newJNull()
discard getTok(p)
of tkCurlyLe:
if depth > DepthLimit:
raiseParseErr(p, "}")
result = newJObject()
discard getTok(p)
while p.tok != tkCurlyRi:
if p.tok != tkString:
raiseParseErr(p, "string literal as key")
var key = p.a
discard getTok(p)
eat(p, tkColon)
var val = parseJson(p, rawIntegers, rawFloats, depth+1)
result[key] = val
if p.tok != tkComma: break
discard getTok(p)
eat(p, tkCurlyRi)
of tkBracketLe:
if depth > DepthLimit:
raiseParseErr(p, "]")
result = newJArray()
discard getTok(p)
while p.tok != tkBracketRi:
result.add(parseJson(p, rawIntegers, rawFloats, depth+1))
if p.tok != tkComma: break
discard getTok(p)
eat(p, tkBracketRi)
of tkError, tkCurlyRi, tkBracketRi, tkColon, tkComma, tkEof:
raiseParseErr(p, "{")
iterator parseJsonFragments*(s: Stream, filename: string = ""; rawIntegers = false, rawFloats = false): JsonNode =
## Parses from a stream `s` into `JsonNodes`. `filename` is only needed
## for nice error messages.
## The JSON fragments are separated by whitespace. This can be substantially
## faster than the comparable loop
## `for x in splitWhitespace(s): yield parseJson(x)`.
## This closes the stream `s` after it's done.
## If `rawIntegers` is true, integer literals will not be converted to a `JInt`
## field but kept as raw numbers via `JString`.
## If `rawFloats` is true, floating point literals will not be converted to a `JFloat`
## field but kept as raw numbers via `JString`.
var p: JsonParser
p.open(s, filename)
try:
discard getTok(p) # read first token
while p.tok != tkEof:
yield p.parseJson(rawIntegers, rawFloats)
finally:
p.close()
proc parseJson*(s: Stream, filename: string = ""; rawIntegers = false, rawFloats = false): JsonNode =
## Parses from a stream `s` into a `JsonNode`. `filename` is only needed
## for nice error messages.
## If `s` contains extra data, it will raise `JsonParsingError`.
## This closes the stream `s` after it's done.
## If `rawIntegers` is true, integer literals will not be converted to a `JInt`
## field but kept as raw numbers via `JString`.
## If `rawFloats` is true, floating point literals will not be converted to a `JFloat`
## field but kept as raw numbers via `JString`.
var p: JsonParser
p.open(s, filename)
try:
discard getTok(p) # read first token
result = p.parseJson(rawIntegers, rawFloats)
eat(p, tkEof) # check if there is no extra data
finally:
p.close()
when defined(js):
from std/math import `mod`
from std/jsffi import JsObject, `[]`, to
from std/private/jsutils import getProtoName, isInteger, isSafeInteger
proc parseNativeJson(x: cstring): JsObject {.importjs: "JSON.parse(#)".}
proc getVarType(x: JsObject, isRawNumber: var bool): JsonNodeKind =
result = JNull
case $getProtoName(x) # TODO: Implicit returns fail here.
of "[object Array]": return JArray
of "[object Object]": return JObject
of "[object Number]":
if isInteger(x) and 1.0 / cast[float](x) != -Inf: # preserve -0.0 as float
if isSafeInteger(x):
return JInt
else:
isRawNumber = true
return JString
else:
return JFloat
of "[object Boolean]": return JBool
of "[object Null]": return JNull
of "[object String]": return JString
else: assert false
proc len(x: JsObject): int =
{.emit: """
`result` = `x`.length;
""".}
proc convertObject(x: JsObject): JsonNode =
var isRawNumber = false
case getVarType(x, isRawNumber)
of JArray:
result = newJArray()
for i in 0 ..< x.len:
result.add(x[i].convertObject())
of JObject:
result = newJObject()
{.emit: """for (var property in `x`) {
if (`x`.hasOwnProperty(property)) {
""".}
var nimProperty: cstring
var nimValue: JsObject
{.emit: "`nimProperty` = property; `nimValue` = `x`[property];".}
result[$nimProperty] = nimValue.convertObject()
{.emit: "}}".}
of JInt:
result = newJInt(x.to(int))
of JFloat:
result = newJFloat(x.to(float))
of JString:
# Dunno what to do with isUnquoted here
if isRawNumber:
var value: cstring
{.emit: "`value` = `x`.toString();".}
result = newJRawNumber($value)
else:
result = newJString($x.to(cstring))
of JBool:
result = newJBool(x.to(bool))
of JNull:
result = newJNull()
proc parseJson*(buffer: string): JsonNode =
when nimvm:
return parseJson(newStringStream(buffer), "input")
else:
return parseNativeJson(buffer).convertObject()
else:
proc parseJson*(buffer: string; rawIntegers = false, rawFloats = false): JsonNode =
## Parses JSON from `buffer`.
## If `buffer` contains extra data, it will raise `JsonParsingError`.
## If `rawIntegers` is true, integer literals will not be converted to a `JInt`
## field but kept as raw numbers via `JString`.
## If `rawFloats` is true, floating point literals will not be converted to a `JFloat`
## field but kept as raw numbers via `JString`.
result = parseJson(newStringStream(buffer), "input", rawIntegers, rawFloats)
proc parseFile*(filename: string): JsonNode =
## Parses `file` into a `JsonNode`.
## If `file` contains extra data, it will raise `JsonParsingError`.
var stream = newFileStream(filename, fmRead)
if stream == nil:
raise newException(IOError, "cannot read from file: " & filename)
result = parseJson(stream, filename, rawIntegers=false, rawFloats=false)
# -- Json deserialiser. --
template verifyJsonKind(node: JsonNode, kinds: set[JsonNodeKind],
ast: string) =
if node == nil:
raise newException(KeyError, "key not found: " & ast)
elif node.kind notin kinds:
let msg = "Incorrect JSON kind. Wanted '$1' in '$2' but got '$3'." % [
$kinds,
ast,
$node.kind
]
raise newException(JsonKindError, msg)
macro isRefSkipDistinct*(arg: typed): untyped =
## internal only, do not use
var impl = getTypeImpl(arg)
if impl.kind == nnkBracketExpr and impl[0].eqIdent("typeDesc"):
impl = getTypeImpl(impl[1])
while impl.kind == nnkDistinctTy:
impl = getTypeImpl(impl[0])
result = newLit(impl.kind == nnkRefTy)
# The following forward declarations don't work in older versions of Nim
# forward declare all initFromJson
proc initFromJson(dst: var string; jsonNode: JsonNode; jsonPath: var string)
proc initFromJson(dst: var bool; jsonNode: JsonNode; jsonPath: var string)
proc initFromJson(dst: var JsonNode; jsonNode: JsonNode; jsonPath: var string)
proc initFromJson[T: SomeInteger](dst: var T; jsonNode: JsonNode, jsonPath: var string)
proc initFromJson[T: SomeFloat](dst: var T; jsonNode: JsonNode; jsonPath: var string)
proc initFromJson[T: enum](dst: var T; jsonNode: JsonNode; jsonPath: var string)
proc initFromJson[T](dst: var seq[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 OrderedTable[string, 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: distinct](dst: var T; jsonNode: JsonNode; jsonPath: var string)
proc initFromJson[T: object|tuple](dst: var T; jsonNode: JsonNode; jsonPath: var string)
# initFromJson definitions
proc initFromJson(dst: var string; jsonNode: JsonNode; jsonPath: var string) =
verifyJsonKind(jsonNode, {JString, JNull}, jsonPath)
# since strings don't have a nil state anymore, this mapping of
# JNull to the default string is questionable. `none(string)` and
# `some("")` have the same potentional json value `JNull`.
if jsonNode.kind == JNull:
dst = ""
else:
dst = jsonNode.str
proc initFromJson(dst: var bool; jsonNode: JsonNode; jsonPath: var string) =
verifyJsonKind(jsonNode, {JBool}, jsonPath)
dst = jsonNode.bval
proc initFromJson(dst: var JsonNode; jsonNode: JsonNode; jsonPath: var string) =
if jsonNode == nil:
raise newException(KeyError, "key not found: " & jsonPath)
dst = jsonNode.copy
proc initFromJson[T: SomeInteger](dst: var T; jsonNode: JsonNode, jsonPath: var string) =
when T is uint|uint64 or int.sizeof == 4:
verifyJsonKind(jsonNode, {JInt, JString}, jsonPath)
case jsonNode.kind
of JString:
let x = parseBiggestUInt(jsonNode.str)
dst = cast[T](x)
else:
dst = T(jsonNode.num)
else:
verifyJsonKind(jsonNode, {JInt}, jsonPath)
dst = cast[T](jsonNode.num)
proc initFromJson[T: SomeFloat](dst: var T; jsonNode: JsonNode; jsonPath: var string) =
verifyJsonKind(jsonNode, {JInt, JFloat, JString}, jsonPath)
if jsonNode.kind == JString:
case jsonNode.str
of "nan":
let b = NaN
dst = T(b)
# dst = NaN # would fail some tests because range conversions would cause CT error
# in some cases; but this is not a hot-spot inside this branch and backend can optimize this.
of "inf":
let b = Inf
dst = T(b)
of "-inf":
let b = -Inf
dst = T(b)
else: raise newException(JsonKindError, "expected 'nan|inf|-inf', got " & jsonNode.str)
else:
if jsonNode.kind == JFloat:
dst = T(jsonNode.fnum)
else:
dst = T(jsonNode.num)
proc initFromJson[T: enum](dst: var T; jsonNode: JsonNode; jsonPath: var string) =
verifyJsonKind(jsonNode, {JString}, jsonPath)
dst = parseEnum[T](jsonNode.getStr)
proc initFromJson[T](dst: var seq[T]; jsonNode: JsonNode; jsonPath: var string) =
verifyJsonKind(jsonNode, {JArray}, jsonPath)
dst.setLen jsonNode.len
let orignalJsonPathLen = jsonPath.len
for i in 0 ..< jsonNode.len:
jsonPath.add '['
jsonPath.addInt i
jsonPath.add ']'
initFromJson(dst[i], jsonNode[i], jsonPath)
jsonPath.setLen orignalJsonPathLen
proc initFromJson[S,T](dst: var array[S,T]; jsonNode: JsonNode; jsonPath: var string) =
verifyJsonKind(jsonNode, {JArray}, jsonPath)
let originalJsonPathLen = jsonPath.len
for i in 0 ..< jsonNode.len:
jsonPath.add '['
jsonPath.addInt i
jsonPath.add ']'
initFromJson(dst[i.S], jsonNode[i], jsonPath) # `.S` for enum indexed arrays
jsonPath.setLen originalJsonPathLen
proc initFromJson[T](dst: var Table[string,T]; jsonNode: JsonNode; jsonPath: var string) =
dst = initTable[string, T]()
verifyJsonKind(jsonNode, {JObject}, jsonPath)
let originalJsonPathLen = jsonPath.len
for key in keys(jsonNode.fields):
jsonPath.add '.'
jsonPath.add key
initFromJson(mgetOrPut(dst, key, default(T)), jsonNode[key], jsonPath)
jsonPath.setLen originalJsonPathLen
proc initFromJson[T](dst: var OrderedTable[string,T]; jsonNode: JsonNode; jsonPath: var string) =
dst = initOrderedTable[string,T]()
verifyJsonKind(jsonNode, {JObject}, jsonPath)
let originalJsonPathLen = jsonPath.len
for key in keys(jsonNode.fields):
jsonPath.add '.'
jsonPath.add key
initFromJson(mgetOrPut(dst, key, default(T)), jsonNode[key], jsonPath)
jsonPath.setLen originalJsonPathLen
proc initFromJson[T](dst: var ref T; jsonNode: JsonNode; jsonPath: var string) =
verifyJsonKind(jsonNode, {JObject, JNull}, jsonPath)
if jsonNode.kind == JNull:
dst = nil
else:
dst = new(T)
initFromJson(dst[], jsonNode, jsonPath)
proc initFromJson[T](dst: var Option[T]; jsonNode: JsonNode; jsonPath: var string) =
if jsonNode != nil and jsonNode.kind != JNull:
when T is ref:
dst = some(new(T))
else:
dst = some(default(T))
initFromJson(dst.get, jsonNode, jsonPath)
macro assignDistinctImpl[T: distinct](dst: var T;jsonNode: JsonNode; jsonPath: var string) =
let typInst = getTypeInst(dst)
let typImpl = getTypeImpl(dst)
let baseTyp = typImpl[0]
result = quote do:
initFromJson(`baseTyp`(`dst`), `jsonNode`, `jsonPath`)
proc initFromJson[T: distinct](dst: var T; jsonNode: JsonNode; jsonPath: var string) =
assignDistinctImpl(dst, jsonNode, jsonPath)
proc detectIncompatibleType(typeExpr, lineinfoNode: NimNode) =
if typeExpr.kind == nnkTupleConstr:
error("Use a named tuple instead of: " & typeExpr.repr, lineinfoNode)
proc foldObjectBody(dst, typeNode, tmpSym, jsonNode, jsonPath, originalJsonPathLen: NimNode) =
case typeNode.kind
of nnkEmpty:
discard
of nnkRecList, nnkTupleTy:
for it in typeNode:
foldObjectBody(dst, it, tmpSym, jsonNode, jsonPath, originalJsonPathLen)
of nnkIdentDefs:
typeNode.expectLen 3
let fieldSym = typeNode[0]
let fieldNameLit = newLit(fieldSym.strVal)
let fieldPathLit = newLit("." & fieldSym.strVal)
let fieldType = typeNode[1]
# Detecting incompatiple tuple types in `assignObjectImpl` only
# would be much cleaner, but the ast for tuple types does not
# contain usable type information.
detectIncompatibleType(fieldType, fieldSym)
dst.add quote do:
jsonPath.add `fieldPathLit`
when nimvm:
when isRefSkipDistinct(`tmpSym`.`fieldSym`):
# workaround #12489
var tmp: `fieldType`
initFromJson(tmp, getOrDefault(`jsonNode`,`fieldNameLit`), `jsonPath`)
`tmpSym`.`fieldSym` = tmp
else:
initFromJson(`tmpSym`.`fieldSym`, getOrDefault(`jsonNode`,`fieldNameLit`), `jsonPath`)
else:
initFromJson(`tmpSym`.`fieldSym`, getOrDefault(`jsonNode`,`fieldNameLit`), `jsonPath`)
jsonPath.setLen `originalJsonPathLen`
of nnkRecCase:
let kindSym = typeNode[0][0]
let kindNameLit = newLit(kindSym.strVal)
let kindPathLit = newLit("." & kindSym.strVal)
let kindType = typeNode[0][1]
let kindOffsetLit = newLit(uint(getOffset(kindSym)))
dst.add quote do:
var kindTmp: `kindType`
jsonPath.add `kindPathLit`
initFromJson(kindTmp, `jsonNode`[`kindNameLit`], `jsonPath`)
jsonPath.setLen `originalJsonPathLen`
when defined js:
`tmpSym`.`kindSym` = kindTmp
else:
when nimvm:
`tmpSym`.`kindSym` = kindTmp
else:
# fuck it, assign kind field anyway
((cast[ptr `kindType`](cast[uint](`tmpSym`.addr) + `kindOffsetLit`))[]) = kindTmp
dst.add nnkCaseStmt.newTree(nnkDotExpr.newTree(tmpSym, kindSym))
for i in 1 ..< typeNode.len:
foldObjectBody(dst, typeNode[i], tmpSym, jsonNode, jsonPath, originalJsonPathLen)
of nnkOfBranch, nnkElse:
let ofBranch = newNimNode(typeNode.kind)
for i in 0 ..< typeNode.len-1:
ofBranch.add copyNimTree(typeNode[i])
let dstInner = newNimNode(nnkStmtListExpr)
foldObjectBody(dstInner, typeNode[^1], tmpSym, jsonNode, jsonPath, originalJsonPathLen)
# resOuter now contains the inner stmtList
ofBranch.add dstInner
dst[^1].expectKind nnkCaseStmt
dst[^1].add ofBranch
of nnkObjectTy:
typeNode[0].expectKind nnkEmpty
typeNode[1].expectKind {nnkEmpty, nnkOfInherit}
if typeNode[1].kind == nnkOfInherit:
let base = typeNode[1][0]
var impl = getTypeImpl(base)
while impl.kind in {nnkRefTy, nnkPtrTy}:
impl = getTypeImpl(impl[0])
foldObjectBody(dst, impl, tmpSym, jsonNode, jsonPath, originalJsonPathLen)
let body = typeNode[2]
foldObjectBody(dst, body, tmpSym, jsonNode, jsonPath, originalJsonPathLen)
else:
error("unhandled kind: " & $typeNode.kind, typeNode)
macro assignObjectImpl[T](dst: var T; jsonNode: JsonNode; jsonPath: var string) =
let typeSym = getTypeInst(dst)
let originalJsonPathLen = genSym(nskLet, "originalJsonPathLen")
result = newStmtList()
result.add quote do:
let `originalJsonPathLen` = len(`jsonPath`)
if typeSym.kind in {nnkTupleTy, nnkTupleConstr}:
# both, `dst` and `typeSym` don't have good lineinfo. But nothing
# else is available here.
detectIncompatibleType(typeSym, dst)
foldObjectBody(result, typeSym, dst, jsonNode, jsonPath, originalJsonPathLen)
else:
foldObjectBody(result, typeSym.getTypeImpl, dst, jsonNode, jsonPath, originalJsonPathLen)
proc initFromJson[T: object|tuple](dst: var T; jsonNode: JsonNode; jsonPath: var string) =
assignObjectImpl(dst, jsonNode, jsonPath)
proc to*[T](node: JsonNode, t: typedesc[T]): T =
## `Unmarshals`:idx: the specified node into the object type specified.
##
## Known limitations:
##
## * Heterogeneous arrays are not supported.
## * Sets in object variants are not supported.
## * Not nil annotations are not supported.
##
runnableExamples:
let jsonNode = parseJson("""
{
"person": {
"name": "Nimmer",
"age": 21
},
"list": [1, 2, 3, 4]
}
""")
type
Person = object
name: string
age: int
Data = object
person: Person
list: seq[int]
var data = to(jsonNode, Data)
doAssert data.person.name == "Nimmer"
doAssert data.person.age == 21
doAssert data.list == @[1, 2, 3, 4]
var jsonPath = ""
result = default(T)
initFromJson(result, node, jsonPath)
when false:
import std/os
var s = newFileStream(paramStr(1), fmRead)
if s == nil: quit("cannot open the file" & paramStr(1))
var x: JsonParser
open(x, s, paramStr(1))
while true:
next(x)
case x.kind
of jsonError:
Echo(x.errorMsg())
break
of jsonEof: break
of jsonString, jsonInt, jsonFloat: echo(x.str)
of jsonTrue: echo("!TRUE")
of jsonFalse: echo("!FALSE")
of jsonNull: echo("!NULL")
of jsonObjectStart: echo("{")
of jsonObjectEnd: echo("}")
of jsonArrayStart: echo("[")
of jsonArrayEnd: echo("]")
close(x)
# { "json": 5 }
# To get that we shall use, obj["json"]