# 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.7.6.

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-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

- `*list-encoder*`
- `*parse-json-arrays-as-vectors*`
- `*parse-json-booleans-as-symbols*`
- `*parse-json-null-as-keyword*`
- `*parse-object-as*`
- `*parse-object-key-fn*`
- `*symbol-key-encoder*`
- `encode`
- `encode-alist`
- `encode-array-element`
- `encode-array-elements`
- `encode-object`
- `encode-object-element`
- `encode-object-elements`
- `encode-object-slots`
- `encode-plist`
- `encode-slots`
- `json-output-stream`
- `make-json-output-stream`
- `no-json-output-context`
- `parse`
- `with-array`
- `with-object`
- `with-object-element`
- `with-output`
- `with-output-to-string*`

### 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.