{"id":28371541,"url":"https://github.com/igrishaev/jsam","last_synced_at":"2026-03-05T22:09:22.396Z","repository":{"id":276747986,"uuid":"929769718","full_name":"igrishaev/jsam","owner":"igrishaev","description":"A lightweight, zero-deps JSON parser and writer","archived":false,"fork":false,"pushed_at":"2025-04-22T14:05:07.000Z","size":209,"stargazers_count":12,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-10-17T06:35:46.751Z","etag":null,"topics":["clojure","java","json"],"latest_commit_sha":null,"homepage":"https://github.com/igrishaev/jsam","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"unlicense","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/igrishaev.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-02-09T11:07:23.000Z","updated_at":"2025-05-19T07:44:05.000Z","dependencies_parsed_at":"2025-04-19T18:37:25.113Z","dependency_job_id":"b2fd57cb-65b3-4474-83f9-dd6e486b3efb","html_url":"https://github.com/igrishaev/jsam","commit_stats":null,"previous_names":["igrishaev/parser-test","igrishaev/jsam"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/igrishaev/jsam","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Fjsam","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Fjsam/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Fjsam/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Fjsam/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/igrishaev","download_url":"https://codeload.github.com/igrishaev/jsam/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Fjsam/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30152100,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-05T21:15:50.531Z","status":"ssl_error","status_checked_at":"2026-03-05T21:15:11.173Z","response_time":93,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","java","json"],"created_at":"2025-05-29T10:12:08.154Z","updated_at":"2026-03-05T22:09:22.388Z","avatar_url":"https://github.com/igrishaev.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n# JSAM\n\n[wiki]: https://metalgear.fandom.com/wiki/Samuel_Rodrigues\n\nA lightweight, zero-deps JSON parser and writer. Named after [Jetstream\nSam][wiki].\n\n- Small: only 14 Java files with no extra libraries;\n- Not the fastest one but is pretty good (see the chart below);\n- Has got its own features, e.g. read and write multiple values;\n- Flexible and extendable.\n\n## Installation\n\nRequires Java version at least 17. Add a new dependency:\n\n~~~clojure\n;; lein\n[com.github.igrishaev/jsam \"0.1.0\"]\n\n;; deps\ncom.github.igrishaev/jsam {:mvn/version \"0.1.0\"}\n~~~\n\nImport the library:\n\n~~~clojure\n(ns org.some.project\n  (:require\n    [jsam.core :as jsam]))\n~~~\n\n## Reading\n\nTo read a string:\n\n~~~clojure\n(jsam/read-string \"[42.3e-3, 123, \\\"hello\\\", true, false, null, {\\\"some\\\": \\\"map\\\"}]\")\n\n[0.0423 123 \"hello\" true false nil {:some \"map\"}]\n~~~\n\nTo read any kind of a source: a file, a URL, a socket, an input stream, a\nreader, etc:\n\n~~~clojure\n(jsam/read \"data.json\") ;; a file named data.json\n(jsam/read (io/input-stream ...))\n(jsam/read (io/reader ...))\n~~~\n\nBoth functions accept an optional map of settings:\n\n~~~clojure\n(jsam/read-string \"...\" {...})\n(jsam/read (io/file ...) {...})\n~~~\n\nHere is a table of options that affect reading:\n\n| option                   | default                 | comment                              |\n|--------------------------|-------------------------|--------------------------------------|\n| `:read-buf-size`         | 8k                      | Size of a buffer to read             |\n| `:temp-buf-scale-factor` | 2                       | Scale factor for an innter buffer    |\n| `:temp-buf-size`         | 255                     | Inner temp buffer initial size       |\n| `:parser-charset`        | UTF-8                   | Must be an instance of `Charset`     |\n| `:arr-supplier`          | `jsam.core/sup-arr-clj` | An object to collect array values    |\n| `:obj-supplier`          | `jsam.core/sup-obj-clj` | An object to collect key-value pairs |\n| `:bigdec?`               | `false`                 | Use BigDecimal when parsing numbers  |\n| `:fn-key`                | `keyword`               | A function to process keys           |\n\nIf you want keys to stay strings, and parse large numbers using `BigDecimal` to\navoid infinite values, this is what you pass:\n\n~~~clojure\n(jsam/read-string \"...\" {:fn-key identity :bigdec? true})\n~~~\n\nWe will discuss suppliers a bit later.\n\n## Writing\n\nTo dump data into a string, use `write-string`:\n\n~~~clojure\n(jsam/write-string {:hello \"test\" :a [1 nil 3 42.123]})\n\n\"{\\\"hello\\\":\\\"test\\\",\\\"a\\\":[1,null,3,42.123]}\"\n~~~\n\nTo write into a destination, which might be a file, an output stream, a writer,\netc, use `write`:\n\n~~~clojure\n(jsam/write \"data2.json\" {:hello \"test\" :a [1 nil 3 42.123]})\n\n;; or\n\n(jsam/write (io/file ...))\n\n;; or\n\n(with-open [writer (io/writer ...)]\n  (jsam/write writer {...}))\n~~~\n\nBoth functions accept a map of options for writing:\n\n| option             | default | comment                          |\n|--------------------|---------|----------------------------------|\n| `:writer-charset`  | UTF-8   | Must be an instance of `Charset` |\n| `:pretty?`         | `false` | Use indents and line breaks      |\n| `:pretty-indent`   | 2       | Indent growth for each level     |\n| `:multi-separator` | `\\n`    | How to split multiple values     |\n\nThis is how you pretty-print data:\n\n~~~clojure\n(jsam/write \"data3.json\"\n            {:hello \"test\" :a [1 {:foo [1 [42] 3]} 3 42.123]}\n            {:pretty? true\n             :pretty-indent 4})\n~~~\n\nThis is what you'll get (maybe needs some further adjustment):\n\n~~~json\n{\n    \"hello\": \"test\",\n    \"a\": [\n        1,\n        {\n            \"foo\": [\n                1,\n                [\n                    42\n                ],\n                3\n            ]\n        },\n        3,\n        42.123\n    ]\n}\n~~~\n\n## Handling Multiple Values\n\nWhen you have 10.000.000 of rows of data to dump into JSON, a regular approach\nis not developer friendly. It leads to a single array with 10M items that you\nread into memory at once. Only few libraries provide facilities to read arrays\nlazily.\n\nIt's much better to dump rows one by one into a stream and then read them one by\none without saturating memory. Here is how you do it:\n\n~~~clojure\n(jsam/write-multi \"data4.json\"\n                  (for [x (range 0 3)]\n                    {:x x}))\n~~~\n\nThe second argument is a collection that might be lazy as well. The content of\nthe file is:\n\n~~~json\n{\"x\":0}\n{\"x\":1}\n{\"x\":2}\n~~~\n\nNow read it back:\n\n~~~clojure\n(doseq [item (jsam/read-multi \"data4.json\")]\n  (println item))\n\n;; {:x 0}\n;; {:x 1}\n;; {:x 2}\n~~~\n\nThe `read-multi` function returns a **lazy** iterable object meaning it won't\nread everything at once. Also, both `write-` and `read-multi` functions are\npretty-print friendly:\n\n~~~clojure\n;; write\n(jsam/write-multi \"data5.json\"\n                  (for [x (range 0 3)]\n                    {:x [x x x]})\n                  {:pretty? true})\n\n;; read\n(doseq [item (jsam/read-multi \"data5.json\")]\n  (println item))\n\n;; {:x [0 0 0]}\n;; {:x [1 1 1]}\n;; {:x [2 2 2]}\n~~~\n\nThe content of the data5.json file:\n\n~~~json\n{\n  \"x\": [\n    0,\n    0,\n    0\n  ]\n}\n{\n  \"x\": [\n    1,\n    1,\n    1\n  ]\n}\n{\n  \"x\": [\n    2,\n    2,\n    2\n  ]\n}\n~~~\n\n## Type Mapping and Extending\n\nThis chapter covers how to control type mapping between Clojure and JSON realms.\n\nWriting is served using a protocol named `jsam.core/IJSON` with a single encidng\nmethod:\n\n~~~clojure\n(defprotocol IJSON\n  (-encode [this writer]))\n~~~\n\nThe default mapping is the following:\n\n| Clojure | JSON   | Comment                   |\n|---------|--------|---------------------------|\n| nil     | null   |                           |\n| String  | string |                           |\n| Boolean | bool   |                           |\n| Number  | number |                           |\n| Ratio   | string | e.g. `(/ 3 2)` -\u003e `\"3/2\"` |\n| Atom    | any    | gets `deref`-ed           |\n| Ref     | any    | gets `deref`-ed           |\n| List    | array  | lazy seqs as well         |\n| Map     | object | keys coerced to strings   |\n| Keyword | string | leading `:` is trimmed    |\n\nAnything else gets encoded like a string using the `.toString` invocation under\nthe hood:\n\n~~~clojure\n(extend-protocol IJSON\n  ...\n  Object\n  (-encode [this ^JsonWriter writer]\n    (.writeString writer (str this)))\n  ...)\n~~~\n\nHere is how you override encoding. Imagine you have a special type `SneakyType`:\n\n~~~clojure\n(deftype SneakyType [a b c]\n\n  ;; some protocols...\n\n  jsam/IJSON\n  (-encode [this writer]\n    (jsam/-encode [\"I used to be a SneakyType\" a b c] writer)))\n~~~\n\nTest it:\n\n~~~clojure\n(let [data1 {:foo (new SneakyType :a \"b\" 42)}\n      string (jsam/write-string data1)]\n  (jsam/read-string string))\n\n;; {:foo [\"I used to be a SneakyType\" \"a\" \"b\" 42]}\n~~~\n\nWhen reading the data, there is a way to specify how array and object values get\ncollected. Options `:arr-supplier` and `:obj-supplier` accept a `Supplier`\ninstance where the `get` method returns instances of `IArrayBuilder` or\n`IObjectBuilder` interfaces. Each interface knows how to add a value into a\ncollection how to finalize it.\n\nDefault implementations build Clojure persistent collections like\n`PersistentVector` or `PersistenHashMap`. There is a couple of Java-specific\nsuppliers that build `ArrayList` and `HashMap`, respectively. Here is how you\nuse them:\n\n~~~clojure\n(jsam/read-string \"[1, 2, 3]\"\n                  {:arr-supplier jsam/sup-arr-java})\n\n;; [1 2 3]\n;; java.util.ArrayList\n\n(jsam/read-string \"{\\\"test\\\": 42}\"\n                  {:obj-supplier jsam/sup-obj-java})\n\n;; {:test 42}\n;; java.util.HashMap\n~~~\n\nHere are some crazy examples that allow to modify data while you build\ncollections. For an array:\n\n~~~clojure\n(let [arr-supplier\n      (reify java.util.function.Supplier\n        (get [this]\n          (let [state (atom [])]\n            (reify org.jsam.IArrayBuilder\n              (conj [this el]\n                (swap! state clojure.core/conj (* el 10)))\n              (build [this]\n                @state)))))]\n\n  (jsam/read-string \"[1, 2, 3]\"\n                    {:arr-supplier arr-supplier}))\n\n;; [10 20 30]\n~~~\n\nAnd for an object:\n\n~~~clojure\n(let [obj-supplier\n      (jsam/supplier\n        (let [state (atom {})]\n          (reify org.jsam.IObjectBuilder\n            (assoc [this k v]\n              (swap! state clojure.core/assoc k (* v 10)))\n            (build [this]\n              @state))))]\n\n  (jsam/read-string \"{\\\"test\\\": 1}\"\n                    {:obj-supplier obj-supplier}))\n\n;; {:test 10}\n~~~\n\n## Benchmarks\n\nJsam doesn't try to gain as much performance as possible; tuning JSON reading\nand writing is pretty challenging. But so far, the library is not as bad as you\nmight think! It's two times slower that Jsonista and slightly slower than\nCheshire. But it's times faster than data.json which is written in pure Clojure\nand thus is so slow.\n\nThe chart below renders my measures of reading a 100MB Json file. Then the data\nread from this file were dumped into a string. It's pretty clear that Jsam is\nnot the best nor the worst one in this competition. I'll keep the question of\nperformance for further work.\n\nMeasured on MacBook M3 Pro 36Gb.\n\n![](/media/benches.svg)\n\n[p-himik]: https://github.com/p-himik\n\nAnother benchmark made by [Eugene Pakhomov][p-himik]. Reading:\n\n| size   | jsam mean | data.json | cheshire | jsonista | jsoniter | charred |\n|--------|-----------|-----------|----------|----------|----------|---------|\n| 10 b   | 182 ns    | 302 ns    | 800 ns   | 230 ns   | 101 ns   | 485 ns  |\n| 100 b  | 827 ns    | 1 µs      | 2 µs     | 1 µs     | 504 ns   | 1 µs    |\n| 1 kb   | 5 µs      | 8 µs      | 9 µs     | 6 µs     | 3 µs     | 5 µs    |\n| 10 kb  | 58 µs     | 108 µs    | 102 µs   | 58 µs    | 36 µs    | 59 µs   |\n| 100 kb | 573 µs    | 1 ms      | 968 µs   | 596 µs   | 379 µs   | 561 µs  |\n\nWriting:\n\n| size   | jsam mean | data.json | cheshire | jsonista | jsoniter | charred |\n|--------|-----------|-----------|----------|----------|----------|---------|\n| 10 b   | 229 ns    | 491 ns    | 895 ns   | 185 ns   | 2 µs     | 326 ns  |\n| 100 b  | 2 µs      | 3 µs      | 2 µs     | 540 ns   | 3 µs     | 351 ns  |\n| 1 kb   | 14 µs     | 14 µs     | 8 µs     | 3 µs     | 8 µs     | 88 ns   |\n| 10 kb  | 192 µs    | 165 µs    | 85 µs    | 29 µs    | 96 µs    | 10 µs   |\n| 100 kb | 2 ms      | 2 ms      | 827 µs   | 325 µs   | 881 µs   | 88 µs   |\n\nMeasured on i7-9700K.\n\n## On Tests\n\nOne can be interested in how this library was tested. Although being considered\nas a simple format, JSON has got plenty of surprises. Jsam has tree sets of\ntests, namely:\n\n[charred]: https://github.com/cnuernber/charred\n[cnuernber]: https://github.com/cnuernber\n\n- basic cases written by me;\n- a large test suite borrowed from the [Charred library][charred]. Many thanks\n  to [Chris Nuernberger][cnuernber] who allowed me to use his code.\n- an extra set of generative tests borrowed from the official\n  `clojure.data.json` library developed by Clojure team.\n\nThese three, I believe, cover most of the cases. Should you face any weird\nbehavior, please let me know.\n\n~~~\n©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©\nIvan Grishaev, 2025. © UNLICENSE ©\n©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©©\n~~~\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Figrishaev%2Fjsam","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Figrishaev%2Fjsam","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Figrishaev%2Fjsam/lists"}