The other answers already show ways to do this with alists, but alternatively you could customize the json decoder to return a more convenient datastructure. Since you want to use them functionally/persistently, the FSet library might be a good choice.
(ql:quickload '(cl-json :fset :alexandria))
(use-package :alexandria)
First, let's customize the decoder to turn json objects into FSET:MAP
s, and json arrays to FSET:SEQ
s.
(defvar *current-array* nil)
(defun beginning-of-array ()
(setf *current-array* (fset:empty-seq)))
(defun array-member (item)
(fset:push-last *current-array* item))
(defun end-of-array ()
*current-array*)
(defvar *current-object* nil)
(defvar *current-object-key* nil)
(defun beginning-of-object ()
(setf *current-object* (fset:empty-map)))
(defun object-key (key)
(setf *current-object-key*
(funcall json:*identifier-name-to-key*
(funcall json:*json-identifier-name-to-lisp* key))))
(defun object-value (value)
(fset:includef *current-object* *current-object-key* value))
(defun end-of-object ()
*current-object*)
(defun call-with-fset-decoder-semantics (function)
(let ((json:*aggregate-scope-variables*
(list* '*current-array* '*current-object* '*current-object-key*
json:*aggregate-scope-variables*)))
(json:bind-custom-vars (:beginning-of-array #'beginning-of-array
:array-member #'array-member
:end-of-array #'end-of-array
:beginning-of-object #'beginning-of-object
:object-key #'object-key
:object-value #'object-value
:end-of-object #'end-of-object)
(funcall function))))
(defmacro with-fset-decoder-semantics (&body body)
`(call-with-fset-decoder-semantics (lambda () ,@body)))
Assuming your JSON looks something like this.
(defvar *json* "{
\"something\": \"foo\",
\"items\": [
{
\"key\": \"value1\",
\"ignore\": [1, 2, 3]
},
{
\"key\": \"value2\",
\"ignore\": [1, 2, 3]
}
],
\"other\": \"bar\"
}")
Now you can decode it into FSet structures. Notice that FSet uses its custom reader syntax for printing objects. #{| (key value)* |}
means a FSET:MAP
and #[ ... ]
is a FSET:SEQ
. You can use FSET:FSET-SETUP-READTABLE
if you want to be able to read them, but it's not necessary. You should create a custom readtable before doing that.
(with-fset-decoder-semantics
(json:decode-json-from-string *json*))
;=> #{|
; (:ITEMS
; #[
; #{| (:KEY "value1") (:IGNORE #[ 1 2 3 ]) |}
; #{| (:KEY "value2") (:IGNORE #[ 1 2 3 ]) |} ])
; (:OTHER "bar")
; (:SOMETHING "foo") |}
You can use FSET:IMAGE
to remove the :IGNORE
key from the items with FSET:LESS
. FSET:WITH
adds a key to the map, or replaces the existing one.
(let ((data (with-fset-decoder-semantics
(json:decode-json-from-string *json*))))
(fset:with data :items (fset:image (rcurry #'fset:less :ignore)
(fset:lookup data :items))))
;=> #{|
; (:ITEMS #[ #{| (:KEY "value1") |} #{| (:KEY "value2") |} ])
; (:OTHER "bar")
; (:SOMETHING "foo") |}
FSet also handles modify macros conveniently:
(let ((data (with-fset-decoder-semantics
(json:decode-json-from-string *json*))))
(fset:imagef (fset:lookup data :items) (rcurry #'fset:less :ignore))
data)
;=> #{|
; (:ITEMS #[ #{| (:KEY "value1") |} #{| (:KEY "value2") |} ])
; (:OTHER "bar")
; (:SOMETHING "foo") |}
This might look at first glance as if it was modifying the data destructively, but it actually isn't.
(let* ((data (with-fset-decoder-semantics
(json:decode-json-from-string *json*)))
(old-data data))
(fset:imagef (fset:lookup data :items) (rcurry #'fset:less :ignore))
(values data old-data))
;=> #{|
; (:ITEMS #[ #{| (:KEY "value1") |} #{| (:KEY "value2") |} ])
; (:OTHER "bar")
; (:SOMETHING "foo") |}
;=> #{|
; (:ITEMS
; #[
; #{| (:KEY "value1") (:IGNORE #[ 1 2 3 ]) |}
; #{| (:KEY "value2") (:IGNORE #[ 1 2 3 ]) |} ])
; (:OTHER "bar")
; (:SOMETHING "foo") |}