This is a fork of ST-JSON with extra utilities to streamline JSON processing and work with JSON from the REPL.
This fork aims to be backwards compatible with ST-JSON. Using src_lisp[:exports code]{(use-package :functional-json)} will work as a drop in replacement for src_lisp[:exports code]{(use-package :st-json)}.
Following are some examples of using functional-json to process JSON. To run the examples first requires loading the required packages:
(ql:quickload '(:functional-json ;; The library itself
:dexador ;; HTTP and HTTPS fetching
:alexandria ;; Utilities (for compose)
))
(use-package :alexandria)Field names can be “strings”, :keywords, or ‘symbols.
(loop
:with url = "https://api.github.com/repos/jl2/functional-json/contents/"
:with json-list = (fj:read-json (dex:get url))
;; This is the sum computed using with-keys acessors
:with total-size = 0
:for json :in (sort json-list #'< :key (fj:jsoλ :size))
;; Sum using at accessor
:summing (fj:at json :size) :into total-size-from-loop
:do
;; Inside with-keys, 'name references the "name" field of json, 'size references the "size" field,
;; and 'type references the "type" field. Symbols and keywords are mapped to lower case strings
;; of the same name - :id maps to the field with name "id", etc. so that name2 and name3 are equivalent to name
;; Note that these are places, meaning the values are settable with (setf)
(fj:with-keys ((name :name) ;; The name field as keyword
(name2 "name") ;; The name field as string
(name3 'name) ;; The name field as symbol
(size "size")
(type 'type)) json
(incf total-size size)
(format t "~a ~s, ~a bytes - also known as ~a and ~a~%" type name size name2 name3))
:finally
(progn
(format t "Total size using with-keys: ~a~%" total-size)
(format t "Total size using at: ~a~%" total-size-from-loop)))
(let* ((url "https://api.github.com/repos/jl2/functional-json/contents/")
(json-list (fj:read-json (dex:get url)))
;; fj:jsoλ creates a function that, when called on a JSON object returns the value stored there.
;; In this case, html-link is a function that, when called on a JSON object, fetchs the "_links" entry, and then fetchs the "html" entry from it.
(html-link (fj:jsoλ :_links :html)))
;; Print the html-link of every object in json-list
(mapc (compose #'print html-link) json-list))
(let* ((url "https://api.github.com/repos/jl2/functional-json/contents/")
(json-list (fj:read-json (dex:get url :want-stream t)))
(json (fj:at json-list 1)))
;; Index into arrays and lists using integers, into
;; objects using strings, symbols, or keywords
;; Print the _links sub-component from the first entry.
(format t "First: ~a~%" (fj:at json-list 1 :_links))
;; Swap fields and make modifications
;; Object references are places and can be modified using setf and
;; related functions like rotatef
(rotatef (fj:at json :_links 'git)
(fj:at json :_links "self") )
(format t "Swapped: ~a~%" (fj:at json :_links))
;; (setf at) doesn't support array/list indexing
;; so the following commented lines would *fail* with an error condition
;;(rotatef (fj:at json-list 0 :_links 'git)
;; (fj:at json-list 0 :_links "self") )
;;(format t "Swapped: ~a~%" (fj:at json-list 0 :_links))
(rotatef (fj:at json :_links :git) (fj:at json :_links :self) )
(format t "Swapped back: ~a~%" (fj:at json-list 1 :_links)))
(let* ((url "https://api.github.com/repos/jl2/functional-json/contents/")
(json-list (fj:read-json (dex:get url :want-stream t)))
(json (first json-list))
(links (fj:atλ json :_links)))
;; Print specified links
(mapc (compose #'print links) '(:git :self :html))
;; Can't setf through (funcall links)
;; but the object will see changes made with fj:with-key bindings and
;; through fj:at
(fj:with-keys ((git :git)
(self :self)) (funcall links)
(format t "~%~%First:~%~a~%~a~%~%"
(funcall links :git) (funcall links :self))
(rotatef git self)
(format t "Swapped:~%~a~%~a~%~%"
(funcall links :git) (fj:at json :_links "self"))
(rotatef git self)
(format t "Swapped back:~%~a%"
(funcall links))));; Start with empty object
(let* ((json (fj:o)))
;; fj:with-keys bindings are setf-able, even if they didn't exist before
(fj:with-keys ((wat :wat)
(obj :obj)) json
(setf wat 43
obj (fj:o :t1 34)))
;; fj:at is setfable
(setf (fj:at json "foo") (fj:o))
(setf (fj:at json :foo :bar) 47)
(setf (fj:at json :bar :foo ) 48)
(setf (fj:at json "bar") (fj:o :new 80
:test3 90))
;; setf multiple levels of nested structure
(setf (fj:at json "bar" "test") (fj:o :new 80
:test3 90))
(setf (fj:at json :key :field :nested) 10)
(setf (fj:at json :key :field :other) 12)
(setf (fj:at json :key "foo" "test") 90)
(print json))
Declare Common Lisp types for JSON objects with certain keys.
;; Declare some JSON types
;; An automobile has a manufacturer with a name and country, a model name, and
;; an engine
(fj:def-jso-type automobile
((:manufacturer :name)
(:manufacturer :country)
:model
:engine))
;; An ice automobile has an engine with a cylinder count and displacement
(fj:def-jso-type ice-automobile
((:engine :cylinder-count)
(:engine :displacement)))
;; An EV automobile has an engine with watts and volts
(fj:def-jso-type ev-automobile
((:engine :watts)
(:engine :volts)))
;; Check JSON types using typecase
(defun what-is-it (car)
(typecase car
(ev-automobile "Electric car")
(ice-automobile "Combustion car")
(automobile "Just a car")))
(let ((bmw (fj:read-json "
{
\"manufacturer\": {
\"name\": \"BMW\",
\"country\": \"Germany\"
},
\"model\": \"M3\",
\"engine\": {
\"cylinder-count\": 6,
\"displacement\": 6.0
}
}"))
(tesla (fj:read-json "
{
\"manufacturer\": {
\"name\": \"Tesla\",
\"country\": \"usa\"
},
\"model\": \"Whatever\",
\"engine\": {
\"watts\": 6,
\"volts\": 120.0
}
}")))
;; Or (typep) to check type
(list (list :bmw
:auto (typep bmw 'automobile)
:ice (typep bmw 'ice-automobile)
:ev (typep bmw 'ev-automobile)
:what (what-is-it bmw))
(list :tesla
:auto (typep tesla 'automobile)
:ice (typep tesla 'ice-automobile)
:ev (typep tesla 'ev-automobile)
:what (what-is-it tesla))))
Right now most query functions only work with jso association lists. Many functions manipulate jso-alist directly.
I’d like to implement a hashtable version of the jso struct with hashtable compatible access methods.
One possibility is to jso a class with getjso and (setf getjso) methods.
BSD
Copyright (c) 2026 Jeremiah LaRocco <jeremiah_larocco@fastmail.com> Copyright (c) Streamtech & Marijn Haverbeke (marijnh@gmail.com)