Skip to content

jl2/functional-json

Repository files navigation

About functional-json

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)}.

Basic Usage

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)

Load JSON data using `fj:read-json` and `fj:with-keys` to access fields.

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

Use `fj:jsoλ` to create access functions.

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

Use `fj:at` to access fields.

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

Use `fj:atλ` to create curried field access functions.

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

Use `fj:at` to build JSON

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

Use `fj:def-jso-type` to define types for JSON objects

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.

License

BSD

Copyright (c) 2026 Jeremiah LaRocco <jeremiah_larocco@fastmail.com> Copyright (c) Streamtech & Marijn Haverbeke (marijnh@gmail.com)

About

Seamlessly work with JSON from Common Lisp

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors