{"id":20499933,"url":"https://github.com/atlas-engineer/njson","last_synced_at":"2025-03-05T19:29:03.687Z","repository":{"id":62416389,"uuid":"534720346","full_name":"atlas-engineer/njson","owner":"atlas-engineer","description":"Common Lisp JSON handling library (not a parser!), with the aim for convenience and brevity.","archived":false,"fork":false,"pushed_at":"2025-01-02T20:18:58.000Z","size":171,"stargazers_count":17,"open_issues_count":9,"forks_count":3,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-01-16T06:58:16.683Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Common Lisp","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/atlas-engineer.png","metadata":{"files":{"readme":"README.org","changelog":null,"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}},"created_at":"2022-09-09T16:28:26.000Z","updated_at":"2025-01-03T06:47:52.000Z","dependencies_parsed_at":"2024-05-02T10:08:01.462Z","dependency_job_id":"62ad6904-f09b-4e67-a451-0c1104ca8f03","html_url":"https://github.com/atlas-engineer/njson","commit_stats":null,"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/atlas-engineer%2Fnjson","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/atlas-engineer%2Fnjson/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/atlas-engineer%2Fnjson/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/atlas-engineer%2Fnjson/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/atlas-engineer","download_url":"https://codeload.github.com/atlas-engineer/njson/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":242090764,"owners_count":20070225,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":[],"created_at":"2024-11-15T18:19:08.117Z","updated_at":"2025-03-05T19:29:03.658Z","avatar_url":"https://github.com/atlas-engineer.png","language":"Common Lisp","readme":"#+TITLE:njson\n\n*A JSON handling framework aiming for convenience and brevity.*\n\nNJSON aims to make it extremely convenient for you to decode,\nvalidate, destructure, process, and encode JSON data, in the minimum\nkeystrokes/minutes possible.\n\n* Getting started\nClone the Git repository:\n#+begin_src sh\n  git clone --recursive https://github.com/atlas-engineer/njson ~/common-lisp/njson\n#+end_src\n\nLoad NJSON (with a jzon backend) in the REPL:\n#+begin_src lisp\n  ;; Show ASDF where NJSON is.\n  (asdf:load-asd #p\"/path/to/checkout/njson.asd\")\n  ;; Load it with ASDF.\n  (asdf:load-system :njson/jzon)\n  ;; Alternatively, load it with Quicklisp.\n  (ql:quickload :njson/jzon)\n#+end_src\n\nAnd start parsing right away, be it from file:\n#+begin_src lisp\n  (njson:decode #p\"/path/to/njson/checkout/tests/test.json\")\n  ;; =\u003e #(1 3.8 T NIL :NULL \"foo\" #(1 2 3) #(\"bar\" T :NULL 1000000)\n  ;;      #\u003cHASH-TABLE :TEST EQUAL :COUNT 1 {100EAB1383}\u003e\n  ;;      #\u003cHASH-TABLE :TEST EQUAL :COUNT 3 {100EAB16D3}\u003e)\n\n#+end_src\n\nor from string:\n#+begin_src lisp\n  (njson:decode \"[\\\"hello\\\", 5]\")\n  ;; =\u003e #(\"hello\", 5)\n#+end_src\n\nor other specializeable types. Default methods support:\n- pathnames,\n- strings,\n- streams.\n\n** Running tests\nGiven NJSON backend-agnostic nature, you can only test every particular backend against the uniform set of tests that NJSON provides. So, to test jzon backend, you can do:\n#+begin_src lisp\n  (asdf:test-system :njson/jzon)\n#+end_src\nAnd, for the CL-JSON backend,\n#+begin_src lisp\n  (asdf:test-system :njson/cl-json)\n#+end_src\n\n* What NJSON is not (and what it is, instead)\n\n** NJSON is not a JSON parsing library.\n\nIt's one level higher: it's a convenience wrapper around your JSON\nparser of choice. NJSON is made in such a way so as to be usable with\nalmost any JSON library out there. The bundled backends are jzon\n(reliable, though new), and CL-JSON (fuzzy yet battle-proven).\n\n- To make NJSON support your preferred JSON parser, you have to\n  specialize as little as two methods: ~decode-from-stream~ and\n  ~encode-to-stream~. If you care about correctness or proper type\n  dispatching, you may also define ~(en|de)code-(to|from)-string~ and\n  ~(en|de)code-(to|from)-file~.\n\n** NJSON is not propagating unnecessary dependencies on you.\n\nThe core (~njson~ ASDF system) has no dependencies due to specifying\nonly the generics to implement.\n\nEvery other dependency is optional and depends on which backend you\nwant to use for parsing.\n\n** NJSON is not the fastest JSON handling solution out there.\n\nPlug-n-play usability and type variety are much higher of a priority\nthan the performance. The types NJSON returns from its methods (and\nthat your own methods extending NJSON should expect/return) are:\n\n- Lisp ~real~-s for JSON numbers.\n- Lisp strings for JSON strings.\n- ~:null~ for JSON ~null~.\n- ~t~ for ~true~ and ~nil~ for ~false~.\n- Vectors for JSON arrays.\n- Hash-tables for JSON objects.\n\nWith this basic (yet disjoint) set of types, you can easily ~typecase~\nover NJSON output and make informed decisions about the JSON you\nhave. Even if it's some couple of CPU work milliseconds slower than\nhandling raw lists. It's faster in human work seconds, which are much\nmore valuable.\n\n** NJSON is not minimalist.\n\nNJSON has strict requirements on the returned data, but this\nstrictness enables a rich set of JSON-handling primitives/helpers. You\ncan\n- ~(:use #:cl #:njson)~ in your packages if you want short and\n  convenient JSON operations there. It's safe, because NJSON shadows\n  no symbols from CL.\n\n- Or you can define a package local nickname for ~:njson/aliases~ to\n  be a mere ~j:~ (using ~trivial-package-local-nicknames~), so that\n  even shorter helpers (just a couple of characters longer than the\n  regular CL constructs) are available:\n#+begin_src lisp\n  (trivial-package-local-nicknames:add-package-local-nickname :j :njson/aliases :YOUR-PACKAGE)\n  ;; And then use it like.\n  (j:get ...)\n  (j:decode ...)\n  (j:if ...)\n  (j:match ...)\n#+end_src\n\nSee the next section for the functions/macros NJSON exports.\n\n* API\n** FUNCTION njson:jget (alias: njson/aliases:get)\n\nGets the value from the JSON object/array indexed by a certain\nkey. Note that the second value is a boolean denoting whether the\nentry under key is found (like in ~gethash~).\n\n#+begin_src lisp\n  (defvar data (njson:decode \"{\\\"key\\\": 5, \\\"second-key\\\": [1, 2, false]}\"))\n  (njson:jget \"key\" data)\n  ;; =\u003e 5, T\n\n  ;; Index using sequence:\n  (njson:jget '(\"second-key\" 1) data)\n  ;; =\u003e 2, T\n\n  ;; Index using JSON Pointer (as pathname):\n  (njson:jget #p\"/second-key/0\" data)\n  ;; =\u003e 1, T\n\n  ;; Modify the element in place:\n  (setf (njson:jget #p\"/second-key/0\" data) 3)\n  ;; Another indexing syntax, for no particular reason:\n  (njson:jget #(\"second-key\" 0) data)\n  ;; =\u003e 3, T\n#+end_src\n\nNote the pathname indexing—it uses the [[https://www.rfc-editor.org/rfc/rfc6901][JSON Pointer]] syntax for indexing convenience.\n\n** FUNCTION njson:jget* (alias: njson/aliases:get*)\n\nA stricter version of =jget= that throws =no-key= error when there's nothing under the given key in the provided object.\n\nWill be merged into =jget= with the next major release.\n\n** FUNCTION njson:jcopy (alias: njson/aliases:copy)\n\nCopies the whole thing it's passed, no mater the nesting, into a fresh new equal object. Makes all the arrays adjustable and fillable for further possibly destructive use.\n\n#+begin_src lisp\n  (defvar data (njson:jget \"key\" (njson:decode \"{\\\"key\\\": 5}\")))\n  ;; =\u003e 5, T\n  (njson:jget \"key\" (njson:jcopy data))\n  ;; =\u003e 5, T\n#+end_src\n\n** FUNCTION njson:jkeys (alias: njson/aliases:keys)\n\nGets all the keys present in the passed object. Integer keys for arrays, string keys for object, error for anything else.\n#+begin_src lisp\n  (njson:jkeys (njson:decode \"{\\\"a\\\": 1, \\\"b\\\": 2}\"))\n  ;; (\"a\" \"b\")\n  (njson:jkeys (njson:decode \"[\\\"a\\\", \\\"b\\\"]\"))\n  ;; (0 1)\n#+end_src\n\n** FUNCTIONS njson:ensure-array, njson:ensure-object (aliases: njson/aliases:ensure-array, njson/aliases:ensure-object)\n\nEnsure that the passed object is turned into array or object (respectively). If ~:convert-objects~ is provided in ~njson:ensure-array~, it creates an array with all the values of object, discarding keys.\n#+begin_src lisp\n  (njson:ensure-array #(1 2 3))\n  ;; #(1 2 3)\n  (njson:ensure-array 3)\n  ;; #(3)\n  (njson:ensure-array (njson:decode \"{\\\"a\\\": 3}\"))\n  ;; #(#\u003chash-table\u003e)\n  (njson:ensure-array (njson:decode \"{\\\"a\\\": 3}\") :convert-objects t)\n  ;; #(3)\n\n  (njson:ensure-object \"key\" #\u003chash-table\u003e)\n  ;; #\u003chash-table\u003e\n  (njson:ensure-object \"key\" 3)\n  ;; #\u003chash-table\u003e with \"key\": 3\n  (njson:ensure-object \"key\" #(1 2 3))\n  ;; #\u003chash-table\u003e with \"key\": #(1 2 3)\n#+end_src\n\n** FUNCTION njson:jtruep (aliases: njson:jtrue-p, njson:jtrue?, njson:truep, njson:true-p, njson:true?)\n\nChecks whether the given value is true (in other words, neither ~false~, nor ~null~) per JSON.\n\nAll the macros below utilize it, so, if you want to change the behavior of those, specialize this function.\n\n** MACRO njson:jwhen (alias: njson/aliases:when)\n\nA regular CL ~when~ made aware of JSON's ~null~ and ~false~.\n\n#+begin_src lisp\n  (njson:jwhen (njson:decode \"null\")\n    \"This is never returned.\")\n  ;; nil\n  (njson:jwhen (njson:decode \"5\")\n    \"This is always returned.\")\n  ;; \"This is always returned\"\n#+end_src\n\n** MACRO njson:if (alias: njson/aliases:if)\n\nA regular Lisp ~if~ aware of JSON truths and lies.\n\n#+begin_src lisp\n  (njson:jif (njson:decode \"5\")\n             \"This is always returned.\"\n             \"This is never returned.\")\n  ;; \"This is always returned\"\n#+end_src\n\n** MACRO njson:jor, njson:jand, njson:jnot (and aliases: njson/aliases:or, njson/aliases:and, njson/aliases:not)\n\nRegular Lisp logic operators, with awareness of JSON values.\n\n** MACRO njson:jbind (alias njson/aliases:bind)\n\nDestructures a JSON object against the provided destructuring pattern. This is most useful for deeply nested JSON structures often returned from old/corporate APIs. One example of such APIs is the Reddit one. To get to the title of the post, one has to go through half a dozen layers of nested objects and arrays:\n#+begin_src js\n  [{\"kind\": \"Listing\",\n    \"data\": {\"children\": [{\"kind\": \"t3\",\n                           \"data\": {\"approved_at_utc\": null,\n                                    \"subreddit\": \"programming\",\n                                    ...\n                                    // Finally, a title!\n                                    \"title\": \"Henry Baker: Meta-circular semantics for Common Lisp special forms\",\n                                    \"link_flair_richtext\": [],\n                                    \"subreddit_name_prefixed\": \"r/programming\",\n                                    ...}}]\n             ...}}\n   ...]\n#+end_src\n\nOne needs a strong destructuring facility with type checking to move through this mess of JSON data. ~jbind~ is exactly this facility. Here's how accessing the title of Reddit post would look like (array patterns access JSON arrays, list patterns access JSON objects) with ~jbind~:\n#+begin_src lisp\n  (njson:jbind #((\"data\" (\"children\" #((\"data\" (\"title\" title))))))\n      ;; Dexador is not a dependency of NJSON, so load it separately\n      (njson:decode\n       (dex:get\n        \"https://www.reddit.com/r/programming/comments/6er9d/henry_baker_metacircular_semantics_for_common.json\"))\n    title)\n  ;; \"Henry Baker: Meta-circular semantics for Common Lisp special forms\"\n#+end_src\n\nSee documentation for more examples.\n\n** MACRO njson:jmatch (alias njson/aliases:match)\n\nMatches/destructures the provided form against patterns one by one, and executes the body of the successfully matching one with the bindings it established. Every pattern and body is essentially a ~jbind~ with checking for destructuring success. The use-case is dispatching over API responses that differ in structure.\n\nTelegram Bot API, for example, has disjoint contents for error responses and success responses:\n- Error responses have \"ok\" key set to false, and keys called \"description\" and \"error_code\".\n- Successful responses have \"ok\" set to true and \"result\" as the payload they return.\n\nGiven these restrictions, we can ~jmatch~ the result of Bot API:\n#+begin_src lisp\n  (njson:jmatch\n   parsed-api-data\n   ((\"ok\" :true \"result\" result)\n    (values t result))\n   ((\"ok\" :false \"error_code\" _ \"description\" description)\n    (values nil description))\n   (t (error \"Malformed data!\")))\n#+end_src\n\nAfter parsing the data, we have clear value distinctions:\n- On success, return (VALUES (EQL T) *) with the payload.\n- On error, return (VALUES NULL \u0026OPTIONAL STRING).\n- And in the exceptional case of malformed data, error out.\n\n~jmatch~ (and ~jbind~) also checks the value matching (see the ~\"ok\" :true~ and ~\"ok\" :false~ parts) with arbitrary JSON atomic type (number, string, ~:true~ (for T), ~:false~ (for NIL), and ~:null~). Arrays and lists are destructuring patterns already, so any value in them can be equality-checked.\n\n** ERROR njson:jerror\n\nAn umbrella class for all the NJSON errors. If you want to play unsafe, simply ignore all of NJSON errors:\n#+begin_src lisp\n  (handler-case\n      (njson:jget ...)\n    ;; Or j:error if you nicknamed njson/aliases.\n    (njson:jerror ()\n      nil))\n#+end_src\n\n** ERROR njson:encode-to-stream-not-implemented, njson:decode-from-stream-not-implemented\n\nThese get thrown when the JSON parsing back-end does not define methods for =njson:encode-to-stream= and =njson:decode-from-stream=. These are the bare minimum a backend should have to work. Adding the string and file methods is nice, but not required.\n\n** ERROR njson:invalid-key\n\nThis gets thrown when you try to index objects with integer indices and arrays with string keys. Because such an indexing wouldn't make sense.\n\nTo allow string indexing for arrays (to make =\"1\"= be recognized as a valid index), you can patch the =njson:jget= method for string indices:\n\n#+begin_src lisp\n  (defmethod njson:jget :around ((index string) (object array))\n    (if (every #'digit-char-p index)\n        (njson:jget (parse-integer index) object)\n        (call-next-method)))\n#+end_src\n\n** ERROR njson:non-indexable\n\nIt doesn't make sense to index a number. This error reinforces the idea.\n\n** ERROR njson:invalid-pointer\n\nThis error is JSON Pointer specific. It's thrown when there's something wrong with the pointer syntax.\n\n** ERROR njson:no-key\n\nThis error is thrown in =njson:jget*= when the indexed object doesn't have the key it's indexed with.\n\n** ERROR njson:value-mismatch\n\nSome value validated in =njson:jbind= didn't match the expected value.\n\n** ERROR njson:deprecated\n\nMarks a certain function as deprecated.\n","funding_links":[],"categories":["Expert Systems"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fatlas-engineer%2Fnjson","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fatlas-engineer%2Fnjson","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fatlas-engineer%2Fnjson/lists"}