The library is available on quicklisp since the 2021-04-11
dist so
you can install with
(ql:quickload :herodotus)
To install from source clone the repo into your lisp sources directory and load with asdf
(asdf:load-system :herodotus)
or use qlot
git herodotus https://github.com/HenryS1/herodotus.git :branch master
This library provides a macro that generates JSON serialisation and deserialisation at the same time as defining a CLOS class.
For example the below definition creates a CLOS class with two fields
x
and y
which have obvious accessors (x
and y
) and initargs
(:x
and :y
).
CL-USER> (herodotus:define-json-model point (x y))
It also defines a package named point-json
with a function for
parsing json point-json:from-json
and also implements a generic method
for writing to json in the herodotus package.
CL-USER> (defvar *point* (point-json:from-json "{ \"x\": 1, \"y\": 2 }"))
*POINT*
CL-USER> (x *point*)
1
CL-USER> (y *point*)
2
CL-USER> (herodotus:to-json *point*)
"{\"x\":1,\"y\":2}"
You can also define classes that have class members using the type
specifier syntax. This block defines two json models tree
and
branch
. A tree
has branch
members and the branch members will be
parsed from json using the parser defined for the tree
.
CL-USER> (herodotus:define-json-model branch (size))
CL-USER> (herodotus:define-json-model tree ((branches branch)))
The syntax (branches branch)
declares that the field named
branches
must be parsed as the type branch
. Json models for nested
classes need to be defined before the models for the classes they are
nested in or an error will be thrown. The error is thrown at macro
expansion time.
CL-USER> (herodotus:define-json-model test-no-parser ((things not-parseable)))
CL-USER> (herodotus:define-json-model test-no-parser ((things not-parseable)))
class-name TEST-NO-PARSER slots ((THINGS NOT-PARSEABLE))
; Evaluation aborted on #<SIMPLE-ERROR "Could not find parser for
; class NOT-PARSEABLE. Please define a json model for it."
; {100599D903}>.
Fields in class definitions are parsed as either nil (if missing from the json), a single instance if the field is not an array and isn’t empty or a vector if the json contains an array of elements.
CL-USER> (herodotus:define-json-model numbers (ns))
CL-USER> (ns (numbers-json:from-json "{ }"))
NIL
CL_USER> (ns (numbers-json:from-json "{ \"ns\": 1 }"))
1
CL-USER> (ns (numbers-json:from-json "{ \"ns\": [1, 2, 3] }"))
#(1 2 3)
The macro define-json-model
has an optional third argument which
specifies the case convention for parsing json fields. The options for
this argument are
:camel-case ;; camelCase
:kebab-case ;; kebab-case
:snake-case ;; snake_case
:screaming-snake-case ;; SCREAMING_SNAKE_CASE
The default value of the argument is :camel-case
. Below is an example
of changing the default case to snake case.
CL-USER> (herodotus:define-json-model snake-case (pet-snake) :snake-case)
CL-USER> (pet-snake (snake-case-json:from-json "{ \"pet_snake\": \"boa\" }"))
"boa"
CL-USER> (herodotus:to-json (snake-case-json:from-json "{ \"pet_snake\": \"boa\" }"))
"{\"pet_snake\":\"boa\"}"
Parsing specific field names can be done using the third argument of a field specifier. If a special field name is provided it doesn’t have to match the name of the slot in the CLOS class and can use any formatting convention.
CL-USER> (herodotus:define-json-model special-case ((unusual-format () "A_very-UniqueNAME"))
CL-USER> (unusual-format (special-case-json:from-json "{ \"A_very-UniqueNAME\": \"Phineas Fog\" }"))
"Phineas Fog"
CL-USER> (herodotus:to-json (special-case-json:from-json "{ \"A_very-UniqueNAME\": \"Phineas Fog\" }"))
"{\"A_very-UniqueNAME\":\"Phineas Fog\"}"
The define-json-model
macro takes three arguments: name
, slots
and an optional argument for case-type
. The name
argument is the
name of the generated CLOS class. The slots
argument is a collection
of slot descriptors and the case-type
argument is a keyword.
Slot descriptors can be either symbols or lists. If a slot descriptor is a symbol then the value of the corresponding CLOS slot will be a deserialised json primitive in lisp form: a number, boolean, string, vector (for arrays), or hash-table (for objects).
If a slot descriptor is a list then first argument is the CLOS slot
name, the second argument is either ()
or the name of a previously
defined json model to deserialise the value of this field to. The
optional third argument is a special case name for this field which
can have custom formatting.
The case-type
keyword argument is one of :screaming-snake-case
,
:snake-case
, :kebab-case
and :camel-case
and defines the
formatting convention for field names in a json object.
The project depends on YASON which does the json parsing and serialisation under the hood, CL-PPCRE for text manipulation during code generation and Alexandria for macro writing utilities.
This project is provided under the MIT license. See the LICENSE file for details.