Jeison is a library for transforming JSON objects (or alists) into EIEIO objects.
It's available on Melpa:
M-x package-install [RET] jeison [RET]
Add this to the header block at the top (see Library-Headers):
;; Package-Requires: ((jeison "1.0.0"))
Don't forget to add require
command as well:
(require 'jeison)
Additionally, you might want to use Cask to manage package dependencies automatically.
Main idea behind jeison is to create an easier way of dealing with deep JSON objects that contain lots and lots of information. Converting those into your own internal structures (or simply working with them) might become a burden pretty fast. Jeison helps to avoid writing tons of boilerplate code and simply declare how to find your data inside of JSON objects.
In order to use jeison one must first define a class using jeison-defclass
. It works absolutely like defclass
from EIEIO, but allows you to add another property to the class slots: :path
. Here is an example:
(jeison-defclass my-first-jeison-class nil
((name :initarg :name :path (personal name last))
(job :initarg :job :path (job title))))
Defining class like this we tell jeison where it should fetch values for name
and job
slots. In JavaScript syntax, it would look similar to the following code:
name = json['personal']['name']['last'];
job = json['job']['title'];
The next step would be transforming actual JSON object into EIEIO object. This is done by function jeison-read
. Let's assume that we have the following JSON object in the Emacs Lisp variable json-person
:
// json-person
{
"personal": {
"address": { },
"name": {
"first": "John",
"last": "Johnson"
}
},
"job": {
"company": "Good Inc.",
"title": "CEO"
},
"skills": [ ]
}
Calling jeison-read
will produce the following results:
(setq person (jeison-read my-first-jeison-class json-person))
(oref person name) ;; => "Johnson"
(oref person job) ;; => "CEO"
jeison-read
also accepts optional third argument that contains a path to sub-object that we should read. For example, we can use jeison just to get one value from JSON object:
(jeison-read 'string json-person '(personal name last)) ;; => "Johnson"
In many cases, classes that we use in code significantly resemble the structure of the source JSON object. This means that :path
will have same value as the corresponding slot's name. In order to avoid this duplication, jeison allows to omit :path
property and use slot's name as a default:
(jeison-defclass default-path-class nil
((first) (last) (full)))
// json-name
{
"name": {
"first": "John",
"last": "Johnson",
"full": "John Johnson"
}
}
(setq name (jeison-read default-path-class json-name '(name)))
(oref name first) ;; => "John"
(oref name last) ;; => "Johnson"
(oref name full) ;; => "John Johnson"
Jeison checks that type that user wants to read from JSON object matches the one that was actually found:
(jeison-read 'string '((x . 1)) 'x) ;; => (jeison-wrong-parsed-type string 1)
EIEIO allows annotating class slots with types. Besides checking the type of the found object, Jeison uses this information for constructing nested objects.
Let's consider the following example:
(jeison-defclass jeison-person nil
((name :initarg :name :path (personal name last))
(job :initarg :job :type jeison-job)))
(jeison-defclass jeison-job nil
((company :initarg :company)
(position :initarg :position :path title)
(location :initarg :location :path (location city))))
In this example, jeison-person
has a slot that has a type of another jeison class: jeison-job
. As the result of this hierarchy, for the next JSON object:
// json-person
{
"personal": {
"address": { },
"name": {
"first": "John",
"last": "Johnson"
}
},
"job": {
"company": "Good Inc.",
"title": "CEO",
"location": {
"country": "Norway",
"city": "Oslo"
}
},
"skills": [ ]
}
We have these results:
(setq person (jeison-read jeison-person json-person))
(oref person name) ;; => "Johnson"
(setq persons-job (oref person job))
(oref persons-job company) ;; => "Good Inc."
(oref persons-job position) ;; => "CEO"
(oref persons-job location) ;; => "Oslo"
Another JSON's basic data type is array. Jeison can deal with arrays in two ways:
- ignore: jeison can behave like it is a normal value and do nothing about it
(jeison-read
t "{\"numbers\": [1, 2, 10, 40, 100]}" 'numbers) ;; => [1 2 10 40 100]
It is a vector by the given path and jeison can simply return it.
However, sometimes we might want to have a list or check that all elements have certain type.
- type-specific processing: jeison can process JSON arrays based on the expected type
(jeison-read '(list-of integer)
"{\"numbers\": [1, 2, 10, 40, 100]}"
'numbers) ;; => (1 2 10 40 100)
EIEIO defines a very handy type list-of
that we can use for processing array elements and checking that they match the corresponding type.
This mechanism also allows jeison to parse lists of nested objects. Let's continue our "John Johnson" example and add skill processing:
(jeison-defclass jeison-person nil
((name :initarg :name :path (personal name last))
(job :initarg :job :type jeison-job)
(skills :initarg :skills :type (list-of jeison-skill))))
(jeison-defclass jeison-job nil
((company :initarg :company)
(position :initarg :position :path title)
(location :initarg :location :path (location city))))
(jeison-defclass jeison-skill nil
((name :initarg :name :type string)
(level :initarg :level :type integer)))
For the following JSON object:
// json-person
{
"personal": {
"address": { },
"name": {
"first": "John",
"last": "Johnson"
}
},
"job": {
"company": "Good Inc.",
"title": "CEO",
"location": {
"country": "Norway",
"city": "Oslo"
}
},
"skills": [
{
"name": "Programming",
"level": 9
},
{
"name": "Design",
"level": 4
},
{
"name": "Communication",
"level": 1
}
]
}
jeison produces these results:
(setq person (jeison-read jeison-person json-person))
(oref person name) ;; => "Johnson"
(mapcar
(lambda (skill)
(format "%s: %i" (oref skill name) (oref skill level)))
(oref person skills)) ;; => ("Programming: 9" "Design: 4" "Communication: 1")
Sometimes, though, it is not required to process the whole list. Sometimes we want just one element, especially in case of heterogeneous arrays. For this use case, jeison supports indices in its :path
properties.
Here is an example:
// json-company
{
"company": {
"name": "Good Inc.",
"CEOs": [
{
"name": {
"first": "Gunnar",
"last": "Olafson"
},
"founder": true
},
{
"name": "TJ-B",
"cool": true
},
{
"name": {
"first": "John",
"last": "Johnson"
}
}
]
}
}
(jeison-read 'boolean json-company '(company CEOs 0 founder)) ;; => t
Unlike arguments to elt
function, indices can be negative. Negative indices have the same semantics as in Python language (enumeration from the end):
(jeison-read 'string json-company '(company CEOs -1 name last)) ;; => "Johnson"
(jeison-read 'boolean json-company '(company CEOs -2 cool)) ;; => t
Additionally, jeison has a feature of transforming existing classes declared with defclass
macro into jeison classes:
(defclass usual-class nil ((x) (y)))
(jeisonify usual-class)
(setq parsed (jeison-read usual-class "{\"x\": 10, \"y\": 20}"))
(oref parsed x) ;; => 10
(oref parsed y) ;; => 20
NOTE 1 even if defclass
included :path
properties, jeison will still use default paths.
NOTE 2 jeison hacks into the structure of EIEIO classes and their slots. If the modified class relies on the purity of slot properties or class options, don't use jeisonify
functionality and create a new class instead.
Often the data in JSON obect is not exactly what we need, sometimes we need to change it before making it usable by our application. In this situation, simply reading it into a class' slot won't be enough. For this (and some other) use cases, jeison provides a feature of functional elements.
Functional elements might have a bit of an obscure syntax, so it might be better to see how it works within real examples.
Let's say we have the following JSON object:
// json-movie
{
"movie": {
"title": "The Shawshank Redemption",
"rating": "10.0"
}
}
Jeison class:
(jeison-defclass jeison-movie nil
((title :initarg :title :type string)
(rating :initarg :rating :type string :path (info rating))))
would do, but rating is a string, which is not a very good type if we want to compare different movies. In order to make this class more user-friendly, functional element can be used as follows:
(jeison-defclass jeison-movie nil
((title :initarg :title :type string)
(rating :initarg :rating :type number
:path (info (string-to-number rating)))))
Parsing this new version of jeison-movie
class from JSON will produce the following results:
(setq movie (jeison-read jeison-movie json-movie 'movie))
(oref movie title) ;; => "The Shawshank Redemption"
(oref movie rating) ;; => 10.0
(> (oref movie rating) 9.8) ;; => t
Functional element (string-to-number rating)
tells jeison that it should fetch whatever data is located by the path rating
, call string-to-number
function with this data as an argument and return that value.
In the most generic case path including a functional element might look like this:
(a (f b1 b2 b3) c)
Describing what is going on with this path is easier with a similar JavaScript code:
// obj is the target JSON object
f(obj['a']['b1'], obj['a']['b2'], obj['a']['b3'])['c']
As you can see from this example, functional elements can have more than one argument. Let's consider the following modification of one of our previous examples:
// json-name
{
"name": {
"first": "John",
"last": "Johnson",
}
}
(defun get-full-name (first last)
(format "%s %s" first last))
(jeison-read 'string json-name
'(name (get-full-name first last))) ;; => "John Johnson"
The same solution could've been implemented a bit differently:
(jeison-read 'string json-name
'((get-full-name (name first) (name last)))) ;; => "John Johnson"
Jeison uses cask. After the installation of cask, install all dependencies and run tests:
$ cask install
$ cask exec ert-runner
All contributions are most welcome!
It might include any help: bug reports, questions on how to use it, feature suggestions, and documentation updates.