I'd like to explore how the json
library could provide a more convenient way to modify JSON files. My previous approach was to read JSON data using read-json
, convert it into mutable hashtables for objects, and then modify the target key-value pairs. With the introduction of read-json*
, this can now be achieved directly. [json] Add `#:mhash?` to `read-json` by NoahStoryM · Pull Request #5163 · racket/racket · GitHub
@notjack suggested designing auxiliary functions like jsexpr-ref
and jsexpr-set
to handle such tasks. To evaluate this idea, I simplified some scenarios I've encountered and tried to find ways to modify JSON files without relying on mutable hashtables.
Here is an example JSON file and corresponding Racket code illustrating the problem:
data.json
{
"root": {
"items": {
"n1": {"data-1": {"d1": 0, "d3": 0}},
"n2": {"data-2": {"d1": 0, "d3": 0}},
"n3": {"data-3": {"d1": 0, "d3": 0}},
"n4": {"data-4": {"d1": 0, "d3": 0}}
},
"metadata": {
"has-key1": false,
"has-key2": true,
"info-state": {
"key": "value"
}
}
}
}
test.rkt
#lang racket/base
(require racket/hash
racket/string
json (submod json for-extension))
(define (read-js [i (current-input-port)])
(read-json* 'read-json i
#:null (json-null)
#:make-object make-hasheq
#:make-list values
#:make-key string->symbol
#:make-string values))
(define data (call-with-input-file "data.json" read-js))
(define root (hash-ref data 'root))
(define items (hash-ref root 'items))
(for ([i (in-range 1 5)])
(define ni (hash-ref items (string->symbol (format "n~a" i))))
(define data-i (hash-ref ni (string->symbol (format "data-~a" i))))
(for ([k '(d1 d2 d3)])
(define v (* 11 i))
(hash-set! data-i k v)))
(define metadata (hash-ref root 'metadata))
(for ([(k v) (in-hash metadata)])
(define s (symbol->string k))
(when (string-prefix? s "has-")
(hash-set! metadata k (not v)))
(when (string-suffix? s "-state")
(for ([i (in-range 1 4)])
(define k (string->symbol (format "s~a" i)))
(hash-set! v k i))))
(hash-union! metadata
#hasheq([has-key1 . #t]
[has-key2 . #t]
[has-key3 . #t]
[new-key1 . "value1"]
[new-key2 . "value2"]
[new-key3 . "value3"])
#:combine (λ (a b) a))
(write-json data #:indent 2)
I also tried solving the problem using lenses (this is my first time using them, so I'm not sure if this is the most appropriate approach). Here's the lens-based implementation:
#lang racket/base
(require racket/hash
racket/string
json
lens
data/queue)
(define q (make-queue))
(define data identity-lens)
(define root (lens-compose (hash-ref-lens 'root) data))
(define items (lens-compose (hash-ref-lens 'items) root))
(for ([i (in-range 1 5)])
(define ni (lens-compose (hash-ref-lens (string->symbol (format "n~a" i))) items))
(define data-i (lens-compose (hash-ref-lens (string->symbol (format "data-~a" i))) ni))
(for ([k '(d1 d2 d3)])
(define v (* 11 i))
(define (get h) h)
(define (set h _) (hash-set h k v))
(enqueue! q (lens-compose (make-lens get set) data-i))))
(define metadata (lens-compose (hash-ref-lens 'metadata) root))
(let ()
(define (get h) h)
(define (set h _)
(for ([(k v) (in-hash h)])
(define s (symbol->string k))
(when (string-prefix? s "has-")
(define (get h) h)
(define (set h _) (hash-set h k (not v)))
(enqueue! q (lens-compose (make-lens get set) metadata)))
(when (string-suffix? s "-state")
(for ([i (in-range 1 4)])
(define k (string->symbol (format "s~a" i)))
(define (get h) h)
(define (set h _) (hash-set v k i))
(enqueue! q (lens-compose (make-lens get set) metadata)))))
h)
(enqueue! q (lens-compose (make-lens get set) metadata)))
(let ()
(define (get h) h)
(define (set h _)
(hash-union h
#hasheq([has-key1 . #t]
[has-key2 . #t]
[has-key3 . #t]
[new-key1 . "value1"]
[new-key2 . "value2"]
[new-key3 . "value3"])
#:combine (λ (a b) a)))
(enqueue! q (lens-compose (make-lens get set) metadata)))
(for/fold ([data (call-with-input-file "data.json" read-json)]
#:result (write-json data #:indent 2))
([l (in-queue q)])
(lens-set l data 'start))