Process JSON Data¶
This recipe covers reading, transforming, and writing JSON — from files, from strings, and from HTTP APIs.
Setup¶
Read JSON from a file¶
(import (scheme base) (scheme write) (scheme file) (kaappi json))
(define (read-json-file path)
(call-with-input-file path json-read))
(define config (read-json-file "config.json"))
(display config)
(newline)
Access nested values¶
JSON objects are alists, arrays are lists. Use assoc to look up keys and
cdr to get the value:
;; Given: {"server": {"host": "localhost", "port": 8080}, "debug": true}
(define config
(json-read-string "{\"server\":{\"host\":\"localhost\",\"port\":8080},\"debug\":true}"))
(cdr (assoc "debug" config))
;=> #t
(cdr (assoc "host" (cdr (assoc "server" config))))
;=> "localhost"
For deeply nested access, a helper avoids chaining assoc/cdr:
(define (json-ref obj . keys)
(let loop ((o obj) (ks keys))
(if (null? ks)
o
(let ((pair (assoc (car ks) o)))
(if pair
(loop (cdr pair) (cdr ks))
#f)))))
(json-ref config "server" "port") ;=> 8080
(json-ref config "missing" "key") ;=> #f
Transform a list of objects¶
Filter and reshape a JSON array:
(define data (json-read-string
"[{\"name\":\"Alice\",\"age\":30,\"active\":true},
{\"name\":\"Bob\",\"age\":25,\"active\":false},
{\"name\":\"Carol\",\"age\":35,\"active\":true}]"))
;; Keep only active users, extract names
(define active-names
(map (lambda (user) (cdr (assoc "name" user)))
(filter (lambda (user) (cdr (assoc "active" user)))
data)))
active-names ;=> ("Alice" "Carol")
Build JSON from Scheme data¶
Quasiquote with unquote makes building JSON structures natural:
(define users '("Alice" "Bob" "Carol"))
(define result
`(("count" . ,(length users))
("users" . ,(map (lambda (name)
`(("name" . ,name)
("name_length" . ,(string-length name))))
users))))
(display (json-write-string result))
;=> {"count":3,"users":[{"name":"Alice","name_length":5},...]}
Write JSON to a file¶
Fetch JSON from an API¶
Combine kaappi-http with kaappi-json to call REST APIs:
(import (kaappi http) (kaappi json))
(define (api-get url)
(let ((resp (http-get url '(("Accept" . "application/json")))))
(if (= (response-status resp) 200)
(json-read-string (response-body resp))
(error "API error" (response-status resp)))))
(define data (api-get "https://httpbin.org/get"))
(display (cdr (assoc "origin" data)))
(newline)
POST JSON to an API¶
(define (api-post url body)
(let ((resp (http-post url
'(("Content-Type" . "application/json"))
(json-write-string body))))
(json-read-string (response-body resp))))
(define result
(api-post "https://httpbin.org/post"
'(("name" . "Alice") ("score" . 95))))
Round-trip: read, transform, write¶
A common pattern — read a JSON file, modify it, write it back:
(import (scheme base) (scheme file) (kaappi json))
(define (update-json-file path transform)
(let ((data (call-with-input-file path json-read)))
(let ((updated (transform data)))
(call-with-output-file path
(lambda (port)
(json-write updated port))))))
;; Add a "processed" field to every item in the array
(update-json-file "items.json"
(lambda (items)
(map (lambda (item)
(cons '("processed" . #t) item))
items)))
Handle null values¶
JSON null is represented as the symbol 'null:
(json-read-string "{\"name\":\"Alice\",\"email\":null}")
;=> (("name" . "Alice") ("email" . null))
(json-null? 'null) ;=> #t
(json-null? "hello") ;=> #f
;; Emit null in output — json-null is the constant 'null
(json-write-string `(("value" . ,json-null)))
;=> "{\"value\":null}"
Checking decoded types¶
Parsed JSON decodes to ordinary Scheme values; test each with a predicate:
| Scheme value | Predicate |
|---|---|
"hello" (string) |
string? |
42 (integer) |
integer? |
3.14 (float) |
inexact? |
#t / #f |
boolean? |
'null |
json-null? |
(1 2 3) (array) |
list? |
(("a" . 1)) (object) |
pair? with a string? car |
For the full JSON-to-Scheme representation table, see the JSON library reference.