Handling JSONB with next.jdbc
- JSONB Columns in PostGreSQL
- jsonista time
- Writing a custom object mapper
- Let's define a custom mapper that would keywordise our JSON to give us the Clojure Map we need.
- Our new
pgobject->map
function - A step further
- Improving the
pgobject->map
function
JSONB Columns in PostGreSQL
I am assuming you'd know what a JSONB column is in PostGres. For those new to Clojure and next.jdbc
, when you query your JSONB column, you'll see something of this sort.:data #object[org.postgresql.util.PGobject 0x2a8a9ebf "{\"key\": \"value\"}"],
This is not in a state which we can readily use in Clojure. We need this to be a Map for it to be useful.
jsonista time
jsonista is a Clojure library for JSON encoding and decoding. We can extract the value from our PgObject and read the string and turn it into a key-value pair of sorts.
(defn pgobject->map
[data]
(json/read-value (.getValue data)))
This would convert our JSONB into something of this sort {"key" "value"}
which is still not quite the Clojure Map we need.
Writing a custom object mapper
Jsonista's read-value function in its first arity takes in just an object and on its second arity, takes in a custom mapper. If you are not sure what an arity is, do read this article which aptly describes what are multi-arity functions and variadic functions in Clojure.
A summary: Arity refers to the number of arguments you can pass a function. You can pass up to two parameters to json/read-value
and if you were to omit the second parameter, it defaults to its default object mapper.
(defn read-value
"Decodes a value from a JSON from anything that
satisfies [[ReadValue]] protocol. By default,
File, URL, String, Reader and InputStream are supported.
To configure, pass in an ObjectMapper created with [[object-mapper]],
see [[object-mapper]] docstring for the available options."
([object]
(-read-value object default-object-mapper))
([object ^ObjectMapper mapper]
(-read-value object mapper)))
Let's define a custom mapper that would keywordise our JSON to give us the Clojure Map we need.
(def keyword-keys-mapper
(json/object-mapper {:decode-key-fn true}))
Do read the docstring for object-mapper to have a detailed explanation. This essentially coerces the keys into Clojure keywords.
Our new pgobject->map
function
(defn pgobject->map
[data]
(json/read-value (.getValue data) keyword-keys-mapper))
This would give us {:key "value"}
Clojure map we need!
A step further
We can have next.jdbc automatically keywordise our JSONB columns by extending the ReadableColumn protocol. Import the namespace (:import (org.postgresql.util PGobject))
(extend-protocol result-set/ReadableColumn
org.postgresql.util.PGobject
(read-column-by-label [^org.postgresql.util.PGobject v _]
(pgobject->map v))
(read-column-by-index [^org.postgresql.util.PGobject v _2 _3]
(pgobject->map v)))
I will not get into the weeds of what extend-protocol
and likewise for ReadableColumn
. The docstring does a great job of that. ^org.postgresql.util.PGobject
is what we call a type hint in Clojure and it exists for performance optimisation purposes. We're just explicitly letting JVM know what's the type for parameter v
. You may omit it. By extending the protocol, we are effectively running our map function each time we read objects from a java.sql.ResultSet
.
Improving the pgobject->map
function
We should make this last amendment to add a bit of type safety to our map function.
(defn pgobject->map
"Transform PGobject containing `json` or `jsonb` value to Clojure
data."
[^org.postgresql.util.PGobject v]
(let [type (.getType v)
value (.getValue v)]
(if (#{"jsonb" "json"} type)
(json/read-value value keyword-keys-mapper)
value)))
That should be all it takes to get your JSONB data automatically processed. You can consume the data immediately as a Map type.