Managing JSON Data with the shasht Common Lisp Library

Parsing JSON Data

The primary mechanism for ingesting and parsing JSON text is the read-json function. It accepts an input source which may be a string, a stream, or nil (defaults to standard input).

(read-json &optional input (eof-error-p t) eof-value single-value-p)

The behavior of the parser is governed by several dynamic variables, allowing customization of the output data structures and type handling:

  • Type Conversion: *read-default-float-format* determines the floating-point format used during parsing.
  • Boolean and Null Handling: Variables like *read-default-true-value* (default t), *read-default-false-value* (default nil), and *read-default-null-value* (default :null) control how JSON literals map to Lisp objects.
  • Container Types: JSON arrays are parsed as vectors by default, configurable via *read-default-array-format* (:vector or :list). JSON objects are parsed as hash tables by default, configurable via *read-default-object-format* (supports :hash-table, :alist, :plist).
  • Safety Limits: *read-length* and *read-level* impose limits on the size and depth of the input structure to prevent denial-of-service attacks.

For convenience, a keyword-argument variant, read-json*, allows binding these dynamic variables lexically within the call:

(read-json* :stream my-stream
            :true-value t
            :false-value nil
            :null-value :null
            :array-format :vector
            :object-format :hash-table
            :float-format 'double-float)

Generating JSON Output

Serialization is handled by the write-json function. It takes a Lisp value and an optional destination (stream, t for standard output, or nil to return a string).

(write-json object &optional (output-stream t))

Output formatting is controlled by dynamic variables:

  • Formatting: *print-pretty* enables pretty-printing, while *write-indent-string* sets the indentation character.
  • Encoding: *write-ascii-encoding* forces Unicode characters to be escaped.
  • Literal Maping: Lists *write-true-values*, *write-false-values*, and *write-null-values* define which Lisp symbols map to boolean and null JSON literals.
  • Structure Mapping: Variables such as *write-alist-as-object*, *write-plist-as-object*, and *write-array-tags* instruct the serializer on how to treat specific list types or tagged lists.

The underlying logic relies on the generic function print-json-value, which can be specialized for custom types. A wrapper function write-json* is also available to manage these configurations via keyword arguments.

Serialization Helpers

For incremental construction of JSON structures, with-json-object and with-json-array macros are provided. Inside an object context, use print-json-key-value to emit fields. Inside an array context, use print-json-value to emit elements.

(with-json-output-to-string (s)
  (with-json-object (s)
    (print-json-key-value "id" 123 s)
    (with-json-array (s "items")
      (print-json-value "apple" s)
      (print-json-value "banana" s))))

Literal Representations

To avoid the overhead of constructing hash tables or vectors for serialization, shasht supports tagged list literals. These are useful for assembling data fragments before transmission.

  • :empty-array: Represents []
  • :empty-object: Represents {}
  • :array: A list tagged with this symbol is treated as an array (e.g., '(:array 1 2 3)).
  • :object-alist: An association list tagged to represent a JSON object.
  • :object-plist: A property list tagged to represent a JSON object.

Example usage:

(write-json
  '(:object-plist
    "config" :empty-object
    "values" (:array 10 20 30)
    "meta" (:object-alist ("version" . "1.0"))))

Note that thece literal representations are unidirectional; they cannot be parsed back into these specific tagged forms by read-json.

Type Mappings

Shasht handles the impedance mismatch between Common Lisp and JSON types through a configurable mapping system.

Numeric Types

JSON numbers without decimal points map to integers. Numbers with decimals or exponents map to floats. The specific float type is controlled by *read-default-float-format*. Rational numbers are converted to floats during output.

Array Handling

Lisp vectors map bidirectionally to JSON arrays. Lists are more complex; due to the ambiguity of nil (empty list vs. false), simply treating lists as arrays requires careful configuration of the false value and array format variables.

To map lists to arrays:

(let ((*read-default-array-format* :list)
      (*write-array-tags* nil)
      (*read-default-false-value* :false)
      (*write-false-values* '(:false)))
  (read-json "[1, 2, 3]")) ;; Returns a list

Object Handling

Hash tables map directly to JSON objects. To use Association Lists (ALISTs) or Property Lists (PLISTs) as the primary representation for JSON objects, the relevant dynamic variables must be set.

Example for ALIST configuration:

(let ((*read-default-object-format* :alist)
      (*write-alist-as-object* t)
      (*read-default-false-value* :false)
      (*write-false-values* '(:false)))
  (read-json "{\"a\": 1}")) ;; Returns an ALIST

Performence and Compatibility

The library includes a comprehensive test suite incorporating the JSONTestSuite to ensure robust handling of edge cases and specification ambiguities. In benchmark tests performed on SBCL, shasht demonstrates competitive read and write speeds compared to other Common Lisp JSON libraries such as cl-json, jsown, and yason, typically balancing speed with feature completeness.

Tags: Common Lisp JSON shasht serialization Data Mapping

Posted on Thu, 13 Aug 2026 16:19:19 +0000 by taurus5_6