JSON is an established alternative to XML as a data interchange format for web applications. YASON implements reading and writing of JSON formatted data in Common Lisp. It does not attempt to provide a mapping between CLOS objects and YASON, but can be used to implement such mappings.
CL-JSON is another Common Lisp package that can be used to work with JSON encoded data. It takes a more integrated approach, providing for library internal mappings between JSON objects and CLOS objects. YASON was created as a lightweight, documented alternative with a minimalistic approach and extensibilty.
YASON has its permanent home at GitHub.
It can be obtained by downloading the release
tarball. The current release is
You may also check out the current development version from its git repository. If you have suggestions regarding YASON, please email me at hans.huebner@gmail.com.
YASON is written in ANSI Common Lisp. It depends on UNIT-TEST, TRIVIAL-GRAY-STREAMS and ALEXANDRIA open source libraries. The recommended way to install YASON and its dependencies is through the excellent Quicklisp library management system.
YASON lives in the :yason package and creates a package nickname :json. Applications will not normally :use this package, but rather use qualified names to access YASON's symbols. For that reason, YASON's symbols do not contain the string "JSON" themselves. See below for usage samples.
As CL-JSON's use of "JSON" as package name has a much longer history and loading of both CL-JSON and YASON into the same image has become more common, the "JSON" nickname was removed from the YASON package with the v0.6.0 release. Users will need to change their applications so that the "JSON" nickname is no longer used to refer to the "YASON" package. It is understood that this is a disruptive change, but as there is no all-encompassing workaround, this step was felt to be the right one to make
| JSON datatype |
CL datatype |
Notes |
|---|---|---|
| object | hash-table :test #'equal |
Keys are strings by default (see
|
| array | list |
Can be changed to read to vectors (see
|
| string | string | JSON escape characters are recognized upon reading. Upon writing, known escape characters are used, but non-ASCII Unicode characters are written as is. |
| number | number | Parsed with READ, printed with PRINC. This is not a faithful implementation of the specification. |
| true | t |
Can be changed to read as TRUE (see
|
| false | nil |
Can be changed to read as FALSE (see
|
| null | nil |
JSON data is always completely parsed into an equivalent
in-memory representation. Upon reading, some translations are
performed by default to make it easier for the Common Lisp
program to work with the data; see
CL-USER> (defvar *json-string* "[{\"foo\":1,\"bar\":[7,8,9]},2,3,4,[5,6,7],true,null]")
*JSON-STRING*
CL-USER> (let* ((result (yason:parse *json-string*)))
(print result)
(alexandria:hash-table-plist (first result)))
(#<HASH-TABLE :TEST EQUAL :COUNT 2 {5A4420F1}> 2 3 4 (5 6 7) T NIL)
("bar" (7 8 9) "foo" 1)
CL-USER> (defun maybe-convert-to-keyword (js-name)
(or (find-symbol (string-upcase js-name) :keyword)
js-name))
MAYBE-CONVERT-TO-KEYWORD
CL-USER> :FOO ; intern the :FOO keyword
:FOO
CL-USER> (let* ((yason:*parse-json-arrays-as-vectors* t)
(yason:*parse-json-booleans-as-symbols* t)
(yason:*parse-object-key-fn* #'maybe-convert-to-keyword)
(result (yason:parse *json-string*)))
(print result)
(alexandria:hash-table-plist (aref result 0)))
#(#<HASH-TABLE :TEST EQUAL :COUNT 2 {59B4EAD1}> 2 3 4 #(5 6 7) YASON:TRUE NIL)
("bar" #(7 8 9) :FOO 1)
The second example modifies the parser's behaviour so that JSON arrays are read as CL vectors, JSON booleans will be read as the symbols TRUE and FALSE and JSON object keys will be looked up in the :keyword package. Interning strings coming from an external source is not recommended practice.
The keyword arguments
Optionally, the JSON that is produced can be indented.
Indentation requires the use of a
In this mode, an in-memory structure is encoded in JSON
format. The structure must consist of objects that are
serializable using the
CL-USER> (yason:encode
(list (alexandria:plist-hash-table
'("foo" 1 "bar" (7 8 9))
:test #'equal)
2 3 4
'(5 6 7)
t nil)
*standard-output*)
[{"foo":1,"bar":[7,8,9]},2,3,4,[5,6,7],true,null]
(#<HASH-TABLE :TEST EQUAL :COUNT 2 {59942D21}> 2 3 4 (5 6 7) T NIL)
This is useful to translate a deeply recursive structure in a single
A useful function that can be bound to this variable is
In this mode, the JSON structure is generated in a stream.
The application makes explicit calls to the encoding library
in order to generate the JSON structure. It provides for more
control over the generated output, and can be used to generate
arbitary JSON without requiring that there exists a directly
matching Lisp data structure. The streaming API uses the
CL-USER> (yason:with-output (*standard-output*)
(yason:with-array ()
(dotimes (i 3)
(yason:encode-array-element i))))
[0,1,2]
NIL
CL-USER> (yason:with-output (*standard-output*)
(yason:with-object ()
(yason:encode-object-element "hello" "hu hu")
(yason:with-object-element ("harr")
(yason:with-array ()
(dotimes (i 3)
(yason:encode-array-element i))))))
{"hello":"hu hu","harr":[0,1,2]}
NIL
(loop for slot in slots
do (encode-object-element (string slot)
(slot-value object slot)))
CL-USER> (defstruct user name age password)
USER
CL-USER> (defmethod yason:encode ((user user) &optional (stream *standard-output*))
(yason:with-output (stream)
(yason:with-object ()
(yason:encode-object-element "name" (user-name user))
(yason:encode-object-element "age" (user-age user)))))
#<STANDARD-METHOD YASON:ENCODE (USER) {5B40A591}>
CL-USER> (yason:encode (list (make-user :name "horst" :age 27 :password "puppy")
(make-user :name "uschi" :age 28 :password "kitten")))
[{"name":"horst","age":27},{"name":"uschi","age":28}]
(#S(USER :NAME "horst" :AGE 27 :PASSWORD "puppy")
#S(USER :NAME "uschi" :AGE 28 :PASSWORD "kitten"))
As you can see, the streaming API and the DOM encoder can be
used together. For an example of the interplay between
(defclass shape () ((color :reader color))) (defclass square (shape) ((side-length :reader side-length))) (defclass circle (shape) ((radius :reader radius)))In order to implement encoding of circles and squares without duplicating code you can specialize
(defmethod yason:encode-slots progn ((shape shape)) (yason:encode-object-element "color" (color shape))) (defmethod yason:encode-slots progn ((square square)) (yason:encode-object-element "side-length" (side-length square))) (defmethod yason:encode-slots progn ((circle circle)) (yason:encode-object-element "radius" (radius circle)))and then use
CL-USER> (yason:with-output-to-string* ()
(yason:encode-object (make-instance 'square :color "red" :side-length 3)))
"{\"color\":\"red\",\"side-length\":3}"
CL-USER> (yason:with-output-to-string* ()
(yason:encode-object (make-instance 'circle :color "blue" :side-length 5)))
"{\"color\":\"blue\",\"radius\":5}"
Alternatively, you can use the shortcut
(defclass person () ((name :reader name :initarg :name) (address :reader address :initarg :address) (phone-number :reader phone-number :initarg :phone) (favorite-color :reader favorite-color :initarg :color))) (defmethod yason:encode-slots progn ((person person)) (yason:encode-object-slots person '(name address phone-number favorite-color)))and then:
CL-USER> (yason:with-output-to-string* ()
(yason:encode-object (make-instance 'person :name "John Doe"
:address "123 Main St."
:phone "(123)-456-7890"
:color "blue")))
"{\"NAME\":\"John Doe\",\"ADDRESS\":\"123 Main St.\",\"PHONE-NUMBER\":\"(123)-456-7890\",
\"FAVORITE-COLOR\":\"blue\"}"
Copyright (c) 2008-2014 Hans Hübner and contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
- Neither the name BKNR nor the names of its contributors may be
used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.