Overview
JSON, read into a tree: RFC 8259, one character at a time.
(import (text json))
(match (parse-json "{\"name\": \"http\", \"tags\": [\"net\", \"web\"]}")
((Ok (JsonObj m)) (map-try-ref m "name")) ; (Some (JsonStr "http"))
((Ok other) None)
((Err why) (println why) None))
A document is read into a Json, a union with a case for each
kind of JSON value. Reading a record straight from JSON, and writing one
as JSON, is (text json-codec)’s: its def/json-type declares
a record and the instances that convert it.
What is read. Exactly RFC 8259: one value, with whitespace
around it and nothing else after it. A byte order mark before the value is
skipped. There are no comments, no trailing commas, no single quotes and
no unquoted keys. A number may not have a leading zero, and needs digits
after its decimal point and its exponent. A control character in a string
has to be escaped, and a \u escape for a surrogate has to be one
half of a pair of them; the pair is read as one character. A document
is UTF-8, as RFC 8259 requires: bytes that are not are refused, at the
line and column where they are.
How it reads. Off the document’s bytes, a chunk at a time:
read-some from a port, or the string’s own bytes for
parse-json. Outside a string everything JSON says is ASCII, so a
byte is a character there; inside one, a character of several bytes is
checked and copied whole.
Numbers. A number without a fraction or an exponent is a
JsonInt, a long. One with either, or one too wide for a
long, is a JsonFloat. So 1 and 1.0 are
different values in the tree; (text json-codec) reads either into
a double, and only the first into an integer.
Objects. An object is a (Map string Json), so the order
its keys were written in is not kept, and when a key is written twice the
last value wins.
Errors. A document that is not JSON is an Err saying
where and why: line 1, column 7: ']' does not begin a value. A
column is a character, so one outside the BMP is one column. Nothing is
raised. A document nested deeper than
json-max-depth is refused the same way rather than exhausting the
stack.
Writing. json->string and write-json write a
Json as text, and written does too. What they write reads
back as the same value: a JsonFloat always has a point or an
exponent, so 2.0 stays a float, and a double is written with the
fewest digits that read back as the same double. The keys of an object
are written in sorted order, because a Map does not keep the order
they were read in. So the same value always gives the same text.
See also: Json, parse-json, read-json
Reference
Types
Json
union ↑ Types
A JSON value.
JsonNull- null.
JsonBool- true or false.
JsonInt- A number with no fraction and no exponent that fits in a long.
JsonFloat- Any other number.
JsonStr- A string, its escapes resolved.
JsonArr- An array, in order.
JsonObj- An object, by key. The order of the keys is not kept; a key written twice keeps its last value.
See also: parse-json
Functions
read-json
function ↑ Functions
(: read-json (-> InputPort (Result string Json)))
(read-json port)
Reads one JSON document from a port.
port- The port. Everything left in it is read: one value, and only space after it.
- returns
- The value, or why the text is not JSON, with its line and column.
A byte order mark before the value is skipped and does not count as a column. On a bjoroutine the port is read without blocking the thread.
See also: parse-json
json->string
function ↑ Functions
(: json->string (-> Json (#:indent int) (#:ascii bool) string))
(json->string j #:indent 0 #:ascii #f)
JSON text for a value: compact, or indented.
j- The value.
#:indent- Spaces for each level. 0, the default, writes everything on one line.
#:ascii- When
#t, each character outside ASCII is written as a\uescape, and a character outside the BMP as a surrogate pair of them. - returns
- The text.
parse-jsonreads it back as the same value.
(json->string (JsonObj (map-set (map-set (map-empty) "b" (JsonInt 1)) "a" (JsonFloat 2.0))))
;; "{\"a\":2.0,\"b\":1}"
(json->string (JsonArr [(JsonStr "knee") JsonNull]) #:indent 2)
;; "[\n \"knee\",\n null\n]"
", \\ and the control characters are escaped in a string,
as RFC 8259 requires. Nothing else is, unless #:ascii #t.
A JsonFloat that is NaN or infinite has no JSON form, and raises
ArgumentException: that is a mistake in the program that made it.
See also: write-json, parse-json
write-json
function ↑ Functions
(: write-json (-> OutputPort Json (#:indent int) (#:ascii bool) void))
(write-json port j #:indent 0 #:ascii #f)
Writes the text json->string gives to a port.
port- The port.
j- The value.
#:indent- As for
json->string. #:ascii- As for
json->string.
The text is made first, and written in one piece. On a bjoroutine the write does not block the thread.
See also: json->string, read-json
parse-json
function ↑ Functions
(: parse-json (-> string (Result string Json)))
(parse-json text)
Reads one JSON document from a string.
text- The document.
- returns
- The value, or why the text is not JSON, with its line and column.
(parse-json "[1, 2.5, null]") ; (Ok (JsonArr [(JsonInt 1) (JsonFloat 2.5) JsonNull]))
(parse-json "[1, 2,]") ; (Err "line 1, column 7: ']' does not begin a value")
See also: read-json