YASON - A JSON encoder/decoder for Common Lisp

Abstract

YASON is a Common Lisp library for encoding and decoding data in the JSON interchange format. JSON is used as a lightweight alternative to XML. YASON has the sole purpose of encoding and decoding data and does not impose any object model on the Common Lisp application that uses it.

Contents

  1. Introduction
  2. Download and Installation
  3. Using JSON as package name
  4. Mapping between JSON and CL datatypes
  5. Parsing JSON data
    1. Parser dictionary
  6. Encoding JSON data
    1. Encoding a JSON DOM
    2. Encoding JSON in streaming mode
    3. Application specific encoders
  7. Symbol index
  8. License
  9. Acknowledgements

Introduction

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.

Download and Installation

YASON has its permanent home at GitHub. It can be obtained by downloading the release tarball. The current release is 0.8.3.

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.

Using JSON as package name

Versions of YASON preceding the v0.6.0 release provided a package nickname "JSON" for the "YASON" package. This made it impossible to load both YASON and CL-JSON into the same image, because CL-JSON uses the "JSON" package name as well.

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

Mapping between JSON and CL datatypes

By default, YASON performs the following mappings between JSON and CL datatypes:
JSON

datatype
CL

datatype
Notes
object hash-table

:test #'equal
Keys are strings by default (see *parse-object-key-fn*). Set *parse-object-as* to :alist in order to have YASON parse objects as alists or to :plist to parse them as plists. When using plists, you probably want to also set *parse-object-key-fn* to a function that interns the object's keys to symbols.
array list Can be changed to read to vectors (see *parse-json-arrays-as-vectors*).
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 *parse-json-booleans-as-symbols*).
false nil Can be changed to read as FALSE (see *parse-json-booleans-as-symbols*).
null nil

Parsing JSON data

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 mapping for details. If desired, the parser can be configured to preserve the full semantics of the JSON data read.

For example
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.

Parser dictionary

[Function]
parse input &key (object-key-fn *parse-object-as-key-fn*) (object-as *parse-object-as*) (json-arrays-as-vectors *parse-json-arrays-as-vectors*) (json-booleans-as-symbols *parse-json-booleans-as-symbols*) (json-nulls-as-keyword *parse-json-null-as-keyword*) => object

Parse input, which must be a string or a stream, as JSON. Returns the Lisp representation of the JSON structure parsed.

The keyword arguments object-key-fn, object-as, json-arrays-as-vectors, json-booleans-as-symbols, and json-null-as-keyword may be used to specify different values for the parsing parameters from the current bindings of the respective special variables.

[Special variable]
*parse-json-arrays-as-vectors*

If set to a true value, JSON arrays will be parsed as vectors, not as lists. NIL is the default.

[Special variable]
*parse-object-as*

Can be set to :hash-table to parse objects as hash tables, :alist to parse them as alists or :plist to parse them as plists. :hash-table is the default.

[Special variable]
*parse-json-booleans-as-symbols*

If set to a true value, JSON booleans will be read as the symbols TRUE and FALSE instead of T and NIL, respectively. NIL is the default.

[Special variable]
*parse-json-null-as-keyword*

If set to a true value, JSON null will be read as the keyword :NULL, instead of NIL. NIL is the default.

[Special variable]
*parse-object-key-fn*

Function to call to convert a key string in a JSON object to a key in the CL hash produced. IDENTITY is the default.

Encoding JSON data

YASON provides two distinct modes to encode JSON data: applications can either create an in-memory representation of the data to be serialized, then have YASON convert it to JSON in one go, or they can use a set of macros to serialze the JSON data element-by-element, allowing fine-grained control over the layout of the generated data.

Optionally, the JSON that is produced can be indented. Indentation requires the use of a JSON-OUTPUT-STREAM as serialization target. With the stream serializer, such a stream is automatically used. If indentation is desired with the DOM serializer, such a stream can be obtained by calling the MAKE-JSON-OUTPUT-STREAM function with the target output string as argument. Please be aware that indented output not requires more space, but is also slower and should not be enabled in performance critical applications.

Encoding a JSON DOM

In this mode, an in-memory structure is encoded in JSON format. The structure must consist of objects that are serializable using the ENCODE function. YASON defines a number of encoders for standard data types (see MAPPING), but the application can define additional methods (e.g. for encoding CLOS objects).

For example:
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)

DOM encoder dictionary

[Generic function]
encode object &optional stream => object

Encode object in JSON format and write to stream. May be specialized by applications to perform specific rendering. Stream defaults to *STANDARD-OUTPUT*.

[Function]
encode-alist object &optional (stream *standard-output*) => object

Encodes object, an alist, in JSON format and write to stream.

[Function]
encode-plist object &optional (stream *standard-output*) => object

Encodes object, a plist, in JSON format and write to stream.

[Function]
make-json-output-stream stream &key (indent t) => stream

Creates a json-output-stream instance that wraps the supplied stream and optionally performs indentation of the generated JSON data. The indent argument is described in WITH-OUTPUT. Note that if the indent argument is NIL, the original stream is returned in order to avoid the performance penalty of the indentation algorithm.

[Special variable]
*list-encoder*

Function to call to translate a CL list into JSON data. 'YASON:ENCODE-PLAIN-LIST-TO-ARRAY is the default; 'YASON:ENCODE-PLIST and 'YASON:ENCODE-ALIST are available to produce JSON objects.

This is useful to translate a deeply recursive structure in a single YASON:ENCODE call.

[Special variable]
*symbol-encoder*

Function to call to translate a CL symbol into a JSON string. The default is to error out, to provide backwards-compatible behaviour.

A useful function that can be bound to this variable is YASON:ENCODE-SYMBOL-AS-LOWERCASE.

[Special variable]
*symbol-key-encoder*

Defines the policy to encode symbols as keys (eg. in hash tables). The default is to error out, to provide backwards-compatible behaviour.

A useful function that can be bound to this variable is YASON:ENCODE-SYMBOL-AS-LOWERCASE.

Encoding JSON in streaming mode

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 encode function, so it is possible to intermix the two (see app-encoders for an example).

For example:
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

Streaming encoder dictionary

[Macro]
with-output (stream &key indent) &body body => result*

Set up a JSON streaming encoder context on stream, then evaluate body. indent can be set to T to enable indentation with a default indentation width or to an integer specifying the desired indentation width. By default, indentation is switched off.

[Macro]
with-output-to-string* (&key indent stream-symbol) &body body => result*

Set up a JSON streaming encoder context on stream-symbol (by default a gensym), then evaluate body. Return a string with the generated JSON output. See WITH-OUTPUT for the description of the indent keyword argument.

[Condition type]
no-json-output-context

This condition is signalled when one of the stream encoding functions is used outside the dynamic context of a WITH-OUTPUT or WITH-OUTPUT-TO-STRING* body.

[Macro]
with-array () &body body => result*

Open a JSON array, then run body. Inside the body, ENCODE-ARRAY-ELEMENT must be called to encode elements to the opened array. Must be called within an existing JSON encoder context (see WITH-OUTPUT and WITH-OUTPUT-TO-STRING*).

[Function]
encode-array-element object => object

Encode object as next array element to the last JSON array opened with WITH-ARRAY in the dynamic context. object is encoded using the ENCODE generic function, so it must be of a type for which an ENCODE method is defined.

[Function]
encode-array-elements &rest objects => result*

Encode objects, a series of JSON encodable objects, as the next array elements in a JSON array opened with WITH-ARRAY. ENCODE-ARRAY-ELEMENTS uses ENCODE-ARRAY-ELEMENT, which must be applicable to each object in the list (i.e. ENCODE must be defined for each object type). Additionally, this must be called within a valid stream context.

[Macro]
with-object () &body body => result*

Open a JSON object, then run body. Inside the body, ENCODE-OBJECT-ELEMENT or WITH-OBJECT-ELEMENT must be called to encode elements to the object. Must be called within an existing JSON encoder WITH-OUTPUT and WITH-OUTPUT-TO-STRING*.

[Macro]
with-object-element (key) &body body => result*

Open a new encoding context to encode a JSON object element. key is the key of the element. The value will be whatever body serializes to the current JSON output context using one of the stream encoding functions. This can be used to stream out nested object structures.

[Function]
encode-object-element key value => value

Encode key and value as object element to the last JSON object opened with WITH-OBJECT in the dynamic context. key and value are encoded using the ENCODE generic function, so they both must be of a type for which an ENCODE method is defined.

[Function]
encode-object-elements &rest elements => result*

Encodes the parameters into JSON in the last object opened with WITH-OBJECT using ENCODE-OBJECT-ELEMENT. The parameters should consist of alternating key/value pairs, and this must be called within a valid stream context.

[Function]
encode-object-slots object slots => result*

Encodes each slot in SLOTS for OBJECT in the last object opened with WITH-OBJECT using ENCODE-OBJECT-ELEMENT. The key is the slot name, and the value is the slot value for the slot on OBJECT. It is equivalent to
(loop for slot in slots
    do (encode-object-element (string slot)
                              (slot-value object slot)))
			

[Function]
encode-slots object => result*

Generic function to encode object slots. There is no default implementation. It should be called in an object encoding context. It uses PROGN combinatation with MOST-SPECIFIC-LAST order, so that base class slots are encoded before derived class slots.

[Function]
encode-object object => result*

Generic function to encode an object. The default implementation opens a new object encoding context and calls ENCODE-SLOTS on the argument.

[Standard class]
json-output-stream

Instances of this class are used to wrap an output stream that is used as a serialization target in the stream encoder and optionally in the DOM encoder if indentation is desired. The class name is not exported, use make-json-output-stream to create a wrapper stream if required.

Application specific encoders

Suppose your application uses structs to represent its data and you want to encode these structs using JSON in order to send them to a client application. Suppose further that your structs also include internal information that you do not want to send. Here is some code that illustrates how one could implement a serialization function:
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. ENCODE invokes itself recursively, so any application defined method will be called while encoding in-memory objects as appropriate.

For an example of the interplay between ENCODE-OBJECT and ENCODE-SLOTS, suppose you have the following CLOS class heirarchy:

(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 ENCODE-SLOTS for all three classes
(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 ENCODE-OBJECT:
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 ENCODE-OBJECT-SLOTS if you want the keys to be the slot names. For example:

(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\"}"

Symbol index

License

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.

Acknowledgements

Thanks go to Edi Weitz for being a great inspiration. This documentation as been generated with a hacked-up version of his DOCUMENTATION-TEMPLATE software. Thanks to David Lichteblau for coining YASON's name.