Hi David, "Thompson, David" <dthomps...@worcester.edu> writes:
> Attached is a patch that adds a new (web json) module. Some may > remember that I submitted a patch back in 2015 (time flies, eh?) for > an (ice-9 json) module that never made it in. Well, 10 years is a long > time and Guile still doesn't have a built-in JSON module. Third party > libraries like guile-json and guile-sjson are available, the latter > being an adaptation of my original patch and the former remaining the > go-to library used by larger Guile projects like Guix. There's also > SRFI-180 (which sounds like a cool surfing trick!) which was published > in 2020 but the API is, in my opinion, overly complicated due to > generators and other things. Anyway, JSON continues to be *the* data > interchange format of the web and Guile really ought to have a simple > API that can read/write JSON to/from a port using only Scheme data > types that have read syntax (i.e. no hash tables like guile-json). > This minimal, practical API is what my patch provides. I've tried my > best to make it as efficient as possible. > > I've settled on the following JSON<->Scheme data type mapping which is > nearly identical to SRFI-180 with the exception of object keys: > > - true and false are #t and #f > - null is the symbol 'null > - numbers are either exact integers (fixnums and bignums) or inexact > reals (flonums, NaNs and infinities excluded) > - strings are strings > - arrays are vectors > - objects are association lists with string keys (SRFI-180 chose > symbols but JSON uses strings so strings feel the most honest) > > Thanks in advance for the review, First of all, let me say thank you for working on that! I agree that this would be most welcome in core Guile, for the reasons you mention. [...] > +@example > +@verbatim > +{ > + "name": "Eva Luator", > + "age": 24, > + "schemer": true, > + "hobbies": [ > + "hacking", > + "cycling", > + "surfing" > + ] > +} > +@end verbatim > +@end example > + > +can be represented with the following Scheme expression: > + > +@example > +@verbatim > +'(("name" . "Eva Luator") > + ("age" . 24) > + ("schemer" . #t) > + ("hobbies" . #("hacking" "cycling" "surfing"))) > +@end verbatim > +@end example Is there particular reason for using vectors instead of plain list to represent JSON arrays? The later would be more idiomatic unless there are technical reasons (perhaps performance?). > +Strings, exact integers, inexact reals (excluding NaNs and infinities), > +@code{#t}, @code{#f}, the symbol @code{null}, vectors, and association > +lists may be serialized as JSON. Association lists serialize as JSON > +objects and vectors serialize as JSON arrays. The keys of association > +lists @emph{must} be strings. > + > +@deffn {Scheme Procedure} read-json [port] > + > +Parse a JSON-encoded value from @var{port} and return its Scheme > +representation. If @var{port} is unspecified, the current input port is > +used. > + > +@example > +@verbatim > +(call-with-input-string "[true,false,null,42,\"foo\"]" read-json) > +;; => #(#t #f null 42 "foo") > + > +(call-with-input-string "{\"foo\":1,\"bar\":2}" read-json) > +;; => (("foo" . 1) ("bar" . 2)) > +@end verbatim > +@end example > + > +@end deffn > + > +@deftp {Exception Type} &json-read-error > +An exception type denoting JSON read errors. > +@end deftp > > +@deffn {Scheme Procedure} write-json exp [port] > + > +Serialize the expression @var{exp} as JSON-encoded text to @var{port}. > +If @var{port} is unspecified, the current output port is used. > + > +@example > +@verbatim > +(with-output-to-string (lambda () (write-json #(#t #f null 42 "foo")))) > +;; => "[true,false,null,42,\"foo\"]" > + > +(with-output-to-string (lambda () (write-json '(("foo" . 1) ("bar" . 2))))) > +;; => "{\"foo\":1,\"bar\":2}" > +@end verbatim > +@end example > + > +@end deffn > + > +@deftp {Exception Type} &json-write-error > +An exception type denoting JSON write errors. > +@end deftp I think it could be a bit nicer if the deffn of read-json and write-json explicitly mentioned that upon error an exception of type X is raised. > + > @node Web Client > @subsection Web Client > > diff --git a/module/web/json.scm b/module/web/json.scm > new file mode 100644 > index 000000000..41aac0e90 > --- /dev/null > +++ b/module/web/json.scm > @@ -0,0 +1,308 @@ > +;;;; json.scm --- JSON reader/writer (ECMA-404) > +;;;; Copyright (C) 2025 Free Software Foundation, Inc. > +;;;; > +;;;; This library is free software; you can redistribute it and/or > +;;;; modify it under the terms of the GNU Lesser General Public > +;;;; License as published by the Free Software Foundation; either > +;;;; version 3 of the License, or (at your option) any later version. > +;;;; > +;;;; This library is distributed in the hope that it will be useful, > +;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of > +;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU > +;;;; Lesser General Public License for more details. > +;;;; > +;;;; You should have received a copy of the GNU Lesser General Public > +;;;; License along with this library; if not, write to the Free Software > +;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA > 02110-1301 USA The FSF has gone office-less, so the above address is now incorrect [0]. The up-to-date template for the copyright notice (header) reads [1]: --8<---------------cut here---------------start------------->8--- This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. --8<---------------cut here---------------end--------------->8--- [0] https://www.fsf.org/blogs/community/fsf-office-closing-party [1] https://www.gnu.org/licenses/gpl-howto.html > + > +(define-module (web json) > + #:use-module (ice-9 exceptions) > + #:use-module (ice-9 match) > + #:use-module (ice-9 textual-ports) > + #:export (&json-read-error > + read-json > + > + &json-write-error > + write-json)) > + > +(define-exception-type &json-read-error &error > + make-json-read-error > + json-read-error?) > + > +(define* (read-json #:optional (port (current-input-port))) > + "Parse a JSON-encoded value from @var{port} and return its Scheme > +representation. If @var{port} is unspecified, the current input port is > +used." > + (define (fail message) > + (raise-exception > + (make-exception (make-json-read-error) > + (make-exception-with-origin 'read-json) > + (make-exception-with-message message) > + (make-exception-with-irritants (list port))))) Hm, I wonder what (list port) looks like in the irritants when the exception is reported; is it useful? Shouldn't it show instead the problematic value? > + (define (consume-whitespace) > + (case (peek-char port) > + ((#\space #\tab #\return #\newline) Should a match + ((? char-whitespace?)) predicate pattern be used here instead, or similar? Or perhaps the above is faster and more self-contained, which can be a good thing. > + (read-char port) > + (consume-whitespace)) > + (else (values)))) > + (define-syntax-rule (define-keyword-reader name str val) > + (define (name) > + (if (string=? (get-string-n port (string-length str)) str) > + val > + (fail "invalid keyword")))) > + (define-keyword-reader read-true "true" #t) > + (define-keyword-reader read-false "false" #f) > + (define-keyword-reader read-null "null" 'null) > + (define (read-hex-digit) > + (case (peek-char port) > + ((#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9) > + (- (char->integer (read-char port)) (char->integer #\0))) > + ((#\a #\b #\c #\d #\e #\f) > + (+ 10 (- (char->integer (read-char port)) (char->integer #\a)))) > + ((#\A #\B #\C #\D #\E #\F) > + (+ 10 (- (char->integer (read-char port)) (char->integer #\A)))) > + (else (fail "invalid hex digit")))) > + (define (read-utf16-character) > + (let* ((a (read-hex-digit)) > + (b (read-hex-digit)) > + (c (read-hex-digit)) > + (d (read-hex-digit))) > + (integer->char (+ (* a (expt 16 3)) (* b (expt 16 2)) (* c 16) d)))) > + (define (read-escape-character) > + (case (read-char port) > + ((#\") #\") > + ((#\\) #\\) > + ((#\/) #\/) > + ((#\b) #\backspace) > + ((#\f) #\page) > + ((#\n) #\newline) > + ((#\r) #\return) > + ((#\t) #\tab) > + ((#\u) (read-utf16-character)) > + (else (fail "invalid escape character")))) > + (define (read-string) > + (read-char port) > + (list->string > + (let lp () > + (match (read-char port) > + ((? eof-object?) (fail "EOF while reading string")) > + (#\" '()) > + (#\\ (cons (read-escape-character) (lp))) > + (char (cons char (lp))))))) > + (define (read-digit-maybe) > + (case (peek-char port) > + ((#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9) > + (- (char->integer (read-char port)) > + (char->integer #\0))) > + (else #f))) > + (define (read-integer) > + (let ((x (read-digit-maybe))) > + (and x > + (let lp ((x x)) > + (match (read-digit-maybe) > + (#f x) > + (y (lp (+ (* x 10) y)))))))) Perhaps the above should be named read-integer-maybe, since it may return #f? > + (define (read-fraction) > + (case (peek-char port) > + ((#\.) > + (read-char port) > + (let lp ((mag 10)) > + (let ((n (read-digit-maybe))) > + (if n (+ (/ n mag) (lp (* mag 10))) 0)))) > + (else 0))) Should the above be named 'read-decimal' ? Does a decimal number in JSON always start with '.' and not with 0. ? I was a bit puzzled on what 'mag' may mean here, I guess 'magnitude' although there doesn't appear to have a clear terminology for it. > + (define (read-exponent) > + (case (peek-char port) > + ((#\e #\E) > + (read-char port) > + (case (peek-char port) > + ((#\-) > + (read-char port) > + (expt 10 (- (read-integer)))) > + ((#\+) > + (read-char port) > + (expt 10 (read-integer))) > + (else > + (expt 10 (read-integer))))) > + (else 1))) > + (define (read-positive-number) > + (let ((n (read-integer))) > + (and n > + (let* ((f (read-fraction)) > + (e (read-exponent)) > + (x (* (+ n f) e))) > + (if (exact-integer? x) x (exact->inexact x)))))) This may return #f. Should it fail instead, or be named read-positive-number-maybe ? > + (define (read-negative-number) > + (read-char port) > + (let ((x (read-positive-number))) > + (if x (- x) (fail "invalid number")))) Not symmetrical with the above: this one would fail if an integer couldn't be read in read-positive-number. > + (define (read-leading-zero-number) > + (read-char port) > + (case (peek-char port) > + ;; Extraneous zeroes are not allowed. A single leading zero > + ;; can only be followed by a decimal point. > + ((#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9 #\e #\E) > + (fail "extraneous leading zero")) Why not check for (not #\.) explicitly? That'd be clearer and would cover all cases (even crazy unexpected ones). > + ;; Fractional number. > + ((#\.) > + (let* ((d (read-fraction)) > + (e (read-exponent))) > + (exact->inexact (* d e)))) > + ;; Just plain zero. > + (else 0))) > + (define (read-key+value-pair) > + (let ((key (read-string))) > + (consume-whitespace) > + (case (read-char port) > + ((#\:) > + (consume-whitespace) > + (cons key (read-value))) > + (else (fail "invalid key/value pair delimiter"))))) > + (define (read-object) > + (read-char port) > + (consume-whitespace) > + (case (peek-char port) > + ;; Empty object. > + ((#\}) > + (read-char port) > + '()) > + (else > + ;; Read first key/value pair, then all subsequent pairs delimited > + ;; by commas. > + (cons (read-key+value-pair) > + (let lp () > + (consume-whitespace) > + (case (peek-char port) > + ((#\,) > + (read-char port) > + (consume-whitespace) > + (cons (read-key+value-pair) (lp))) > + ;; End of object. > + ((#\}) > + (read-char port) > + '()) > + (else (fail "invalid object delimiter")))))))) > + (define (read-array) > + (read-char port) > + (consume-whitespace) > + (case (peek-char port) > + ;; Empty array. > + ((#\]) > + (read-char port) > + #()) > + (else > + (list->vector As mentioned above, just a plain list would be more Schemey, no? What does the vector type buys us? A user wanting a vector could always call list->vector themselves, and otherwise we save some computation. > + ;; Read the first element, then all subsequent elements > + ;; delimited by commas. > + (cons (read-value) > + (let lp () > + (consume-whitespace) > + (case (peek-char port) > + ;; Elements are comma delimited. > + ((#\,) > + (read-char port) > + (consume-whitespace) > + (cons (read-value) (lp))) > + ;; End of array. > + ((#\]) > + (read-char port) > + '()) > + (else (fail "invalid array delimiter"))))))))) > + (define (read-value) > + (consume-whitespace) > + (case (peek-char port) > + ((#\") (read-string)) > + ((#\{) (read-object)) > + ((#\[) (read-array)) > + ((#\t) (read-true)) > + ((#\f) (read-false)) > + ((#\n) (read-null)) > + ((#\-) (read-negative-number)) > + ((#\0) (read-leading-zero-number)) > + ((#\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9) (read-positive-number)) > + (else (fail "invalid value")))) > + (read-value)) > + > +(define-exception-type &json-write-error &error > + make-json-write-error > + json-write-error?) > + > +(define* (write-json exp #:optional (port (current-output-port))) > + "Serialize the expression @var{exp} as JSON-encoded text to @var{port}. > +If @var{port} is unspecified, the current output port is used." > + (define (fail message x) > + (raise-exception > + (make-exception (make-json-write-error) > + (make-exception-with-origin 'write-json) > + (make-exception-with-message message) > + (make-exception-with-irritants (list x))))) > + (define (write-char/escape char) > + (match char > + (#\" (put-string port "\\\"")) > + (#\\ (put-string port "\\\\")) > + (#\/ (put-string port "\\/")) > + (#\backspace (put-string port "\\b")) > + (#\page (put-string port "\\f")) > + (#\newline (put-string port "\\n")) > + (#\return (put-string port "\\r")) > + (#\tab (put-string port "\\t")) > + (_ (put-char port char)))) > + (define (write-string str) > + (let ((in (open-input-string str))) Looks like the above 'in' binding is not used. > + (put-char port #\") > + (string-for-each write-char/escape str) > + (put-char port #\"))) > + (define (write-pair x) > + (match x > + (((? string? key) . value) > + (write-string key) > + (put-char port #\:) > + (write-value value)) > + (_ (fail "invalid key/value pair" x)))) > + (define (write-object obj) > + (put-char port #\{) > + (match obj > + ((head . rest) > + (write-pair head) > + (let lp ((obj rest)) > + (match obj > + (() (values)) Any reason to return (values) instead of some dummy #t to denote 'no-op' ?. > + ((head . rest) > + (put-char port #\,) > + (write-pair head) > + (lp rest)) > + (_ (fail "invalid object" obj)))))) > + (put-char port #\})) > + (define (write-array v) > + (put-char port #\[) > + (match (vector-length v) > + (0 (values)) > + (n > + (write-value (vector-ref v 0)) > + (do ((i 1 (1+ i))) > + ((= i n)) > + (put-char port #\,) > + (write-value (vector-ref v i))))) I suppose the above is more efficient than a for-each loop? I'd be curious to see it profiled, if you still have data. At least now I see than for > 100k, vector-ref is faster than list-ref, which probably explains why you went with vectors (could still be an implementation detail with the list->vector call left in the writer though, in my opinion). > + (put-char port #\])) > + (define (write-number x) > + (if (or (exact-integer? x) > + (and (real? x) > + (inexact? x) > + ;; NaNs and infinities are not allowed. > + (not (or (nan? x) (inf? x))))) > + ;; Scheme's string representations of exact integers and floats > + ;; are compatible with JSON. > + (put-string port (number->string x)) > + (fail "invalid number" x))) > + (define (write-value x) > + (match x > + (#t (put-string port "true")) > + (#f (put-string port "false")) > + ('null (put-string port "null")) > + (() (put-string port "{}")) > + ((? pair?) (write-object x)) > + ((? vector?) (write-array x)) > + ((? string?) (write-string x)) > + ((? number?) (write-number x)) > + (_ (fail "invalid value" x)))) > + (write-value exp)) Phew. That's a pretty low-level parser! I hope it's fast, otherwise it seems it'd be more concise/fun/maintainable to devise a PEG-based one, which appears to be doable for JSON, from what I've read. Perhaps sprinkle with a few performance-related comments where such concerns impacted the design choices, so that we can remember and retest/reverify these in the future when Guile evolves. > diff --git a/test-suite/Makefile.am b/test-suite/Makefile.am > index 6014b1f1f..00afea142 100644 > --- a/test-suite/Makefile.am > +++ b/test-suite/Makefile.am > @@ -73,6 +73,7 @@ SCM_TESTS = tests/00-initial-env.test \ > tests/iconv.test \ > tests/import.test \ > tests/interp.test \ > + tests/json.test \ > tests/keywords.test \ > tests/list.test \ > tests/load.test \ > diff --git a/test-suite/tests/json.test b/test-suite/tests/json.test > new file mode 100644 > index 000000000..f92eeccec > --- /dev/null > +++ b/test-suite/tests/json.test > @@ -0,0 +1,154 @@ > +;;;; json.test --- test JSON reader/writer -*- scheme -*- > +;;;; > +;;;; Copyright (C) 2015 Free Software Foundation, Inc. > +;;;; > +;;;; This library is free software; you can redistribute it and/or > +;;;; modify it under the terms of the GNU Lesser General Public > +;;;; License as published by the Free Software Foundation; either > +;;;; version 3 of the License, or (at your option) any later version. > +;;;; > +;;;; This library is distributed in the hope that it will be useful, > +;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of > +;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU > +;;;; Lesser General Public License for more details. > +;;;; > +;;;; You should have received a copy of the GNU Lesser General Public > +;;;; License along with this library; if not, write to the Free Software > +;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA > 02110-1301 USA > + > +(define-module (test-suite test-json) > + #:use-module (test-suite lib) > + #:use-module (web json)) > + > +;;; > +;;; Reader > +;;; > + > +(define (read-json-string str) > + (call-with-input-string str read-json)) > + > +(define (json-read=? str x) > + (= x (read-json-string str))) > + > +(define (json-read-eq? str x) > + (eq? x (read-json-string str))) > + > +(define (json-read-equal? str x) > + (equal? x (read-json-string str))) > + > +(define (json-read-string=? str x) > + (string=? x (read-json-string str))) > + > +(with-test-prefix "read-json" > + ;; Keywords > + (pass-if (json-read-eq? "true" #t)) > + (pass-if (json-read-eq? "false" #f)) > + (pass-if (json-read-eq? "null" 'null)) > + ;; Numbers > + (pass-if (json-read=? "0" 0)) > + (pass-if (json-read=? "-0" 0)) > + (pass-if (json-read=? "0.0" 0.0)) > + (pass-if (json-read=? "-0.0" -0.0)) > + (pass-if (json-read=? "0.1" 0.1)) > + (pass-if (json-read=? "1.234" 1.234)) > + (pass-if (json-read=? "1" 1)) > + (pass-if (json-read=? "-1" -1)) > + (pass-if (json-read=? "1.1" 1.1)) > + (pass-if (json-read=? "1e2" 1e2)) > + (pass-if (json-read=? "1.1e2" 1.1e2)) > + (pass-if (json-read=? "1.1e-2" 1.1e-2)) > + (pass-if (json-read=? "1.1e+2" 1.1e2)) > + ;; Extraneous zeroes in fraction > + (pass-if (json-read=? "1.000" 1)) > + (pass-if (json-read=? "1.5000" 1.5)) > + ;; Extraneous zeroes in exponent > + (pass-if (json-read=? "1.1e000" 1.1)) > + (pass-if (json-read=? "1.1e-02" 1.1e-2)) > + (pass-if (json-read=? "1.1e+02" 1.1e2)) > + ;; Strings > + (pass-if (json-read-string=? "\"foo\"" "foo")) > + ;; Escape codes > + (pass-if (json-read-string=? "\"\\\"\"" "\"")) > + (pass-if (json-read-string=? "\"\\\\\"" "\\")) > + (pass-if (json-read-string=? "\"\\/\"" "/")) > + (pass-if (json-read-string=? "\"\\b\"" "\b")) > + (pass-if (json-read-string=? "\"\\f\"" "\f")) > + (pass-if (json-read-string=? "\"\\n\"" "\n")) > + (pass-if (json-read-string=? "\"\\r\"" "\r")) > + (pass-if (json-read-string=? "\"\\t\"" "\t")) > + ;; Unicode in hexadecimal format > + (pass-if (json-read-string=? "\"\\u12ab\"" "\u12ab")) > + ;; Objects > + (pass-if (json-read-equal? "{}" '())) > + (pass-if (json-read-equal? "{ \"foo\": \"bar\", \"baz\": \"frob\"}" > + '(("foo" . "bar") ("baz" . "frob")))) > + ;; Nested objects > + (pass-if (json-read-equal? "{\"foo\":{\"bar\":\"baz\"}}" > + '(("foo" . (("bar" . "baz")))))) > + ;; Arrays > + (pass-if (json-read-equal? "[]" #())) > + (pass-if (json-read-equal? "[1, 2, \"foo\"]" > + #(1 2 "foo"))) > + ;; Nested arrays > + (pass-if (json-read-equal? "[1, 2, [\"foo\", \"bar\"]]" > + #(1 2 #("foo" "bar")))) > + ;; Arrays and objects nested within each other > + (pass-if (json-read-equal? "{\"foo\":[{\"bar\":true},{\"baz\":[1,2,3]}]}" > + '(("foo" . #((("bar" . #t)) > + (("baz" . #(1 2 3)))))))) > + ;; Leading whitespace > + (pass-if (json-read-eq? "\t\r\n true" #t))) > +;;; > +;;; Writer > +;;; > + > +(define (write-json-string exp) > + (call-with-output-string > + (lambda (port) > + (write-json exp port)))) > + > +(define (json-write-string=? exp str) > + (string=? str (write-json-string exp))) > + > +(with-test-prefix "write-json" > + ;; Keywords > + (pass-if (json-write-string=? #t "true")) > + (pass-if (json-write-string=? #f "false")) > + (pass-if (json-write-string=? 'null "null")) > + ;; Numbers > + (pass-if (json-write-string=? 0 "0")) > + (pass-if (json-write-string=? 0.0 "0.0")) > + (pass-if (json-write-string=? 0.1 "0.1")) > + (pass-if (json-write-string=? 1 "1")) > + (pass-if (json-write-string=? -1 "-1")) > + (pass-if (json-write-string=? 1.1 "1.1")) > + ;; Strings > + (pass-if (json-write-string=? "foo" "\"foo\"")) > + ;; Escape codes > + (pass-if (json-write-string=? "\"" "\"\\\"\"")) > + (pass-if (json-write-string=? "\\" "\"\\\\\"")) > + (pass-if (json-write-string=? "/" "\"\\/\"")) > + (pass-if (json-write-string=? "\b" "\"\\b\"")) > + (pass-if (json-write-string=? "\f" "\"\\f\"")) > + (pass-if (json-write-string=? "\n" "\"\\n\"")) > + (pass-if (json-write-string=? "\r" "\"\\r\"")) > + (pass-if (json-write-string=? "\t" "\"\\t\"")) > + ;; Objects > + (pass-if (json-write-string=? '() "{}")) > + (pass-if (json-write-string=? '(("foo" . "bar") ("baz" . "frob")) > + "{\"foo\":\"bar\",\"baz\":\"frob\"}")) > + ;; Nested objects > + (pass-if (json-write-string=? '(("foo" . (("bar" . "baz")))) > + "{\"foo\":{\"bar\":\"baz\"}}")) > + ;; Arrays > + (pass-if (json-write-string=? #() "[]")) > + (pass-if (json-write-string=? #(1 2 "foo") > + "[1,2,\"foo\"]")) > + ;; Nested arrays > + (pass-if (json-write-string=? #(1 2 #("foo" "bar")) > + "[1,2,[\"foo\",\"bar\"]]")) > + ;; Arrays and objects nested in each other > + (pass-if (json-write-string=? '(("foo" . #((("bar" . #t)) > + (("baz" . #(1 2)))))) > + > "{\"foo\":[{\"bar\":true},{\"baz\":[1,2]}]}"))) Neat. Nitpick: perhaps add a trailing '.' after each stand-alone comments, to follow existing conventions. I hope my armchair commentary is of some use :-). Thanks again for working on a JSON parser/writer for Guile. -- Thanks, Maxim