If you want to perform updates in-place -- meaning you care more about performance than safety -- this can be accomplished using java.io.RandomAccessFile:
(import '[java.io RandomAccessFile])
(defn append-to-json-list-in-file [file-name new-json-text]
(let [raf (RandomAccessFile. file-name "rw")
lock (.lock (.getChannel raf)) ;; avoid concurrent invocation across processes
current-length (.length raf)]
(if (= current-length 0)
(do
(.writeBytes raf "[\n") ;; On the first write, prepend a "["
(.writeBytes raf new-json-text) ;; ...before the data...
(.writeBytes raf "\n]\n")) ;; ...and a final "\n]\n"
(do
(.seek raf (- current-length 3)) ;; move to before the last "\n]\n"
(.writeBytes raf ",\n") ;; put a comma where that "\n" used to be
(.writeBytes raf new-json-text) ;; ...then the new data...
(.writeBytes raf "\n]\n"))) ;; ...then a new "\n]\n"
(.close lock)
(.close raf)))
As an example of usage -- if no preexisting out.txt
exists, then the result of the following three calls:
(append-to-json-list-in-file "out.txt" "{\"hello\": \"birds\"}")
(append-to-json-list-in-file "out.txt" "{\"hello\": \"trees\"}")
(append-to-json-list-in-file "out.txt" "{\"goodbye\": \"world\"}")
...will be a file containing:
[
{"hello": "birds"},
{"hello": "trees"},
{"goodbye": "world"}
]
Note that the locking prevents multiple processes from calling this code at once with the same output file. It doesn't provide safety from multiple threads in the same process doing concurrent invocations -- if you want that, I'd suggest using an Agent or other inherently-single-threaded construct.
There's also some danger that this could corrupt a file that has been edited by other software -- if a file ends with "\n]\n\n\n"
instead of "\n]\n"
, for example, then seeking to three bytes before the current length would put us in the wrong place, and we'd generate malformed output.
If instead you care more about ensuring that output is complete and not corrupt, the relevant techniques are not JSON-specific (and call for rewriting the entire output file, rather than incrementally updating it); see Atomic file replacement in Clojure.