Clojure: case conversion and boundaries
Inconsistent case is a problems that tends to come up at application boundaries in your software stack. For example your Clojure codebase might use kebab-case
for keywords, whilst your database uses snake_case
for column names and your client wants camelCase
in its json responses. Often, conventions and/or technical limitations prevent you from simply having a single case throughout your entire stack.
One "solution" to this problem is to accept the fact that your app will have a mix of cases. However, this can lead to mistake and frustration, does this function expect customer-id
, cutomerId
or customer_id
? What format does our mobile client expect? A more practical solution to this problem, the one this article will cover, is to add automatic case conversion at these boundaries in your software stack.
Converting the case of a key
Let's start by writing a simple case conversion function for converting kebab-case
keywords to camelCase
keywords.
(defn kebab-case->camelCase [k]
(let [words (clojure.string/split (name k) #"-")]
(->> (map clojure.string/capitalize (rest words))
(apply str (first words))
keyword)))
(kebab-case->camelCase :foo-bar-baz)
=> :fooBarBaz
Converting the case of keys in a map
Now that we have a function for converting case let's convert all the keys of a map using the map-keys
function we implemented in this article.
(defn map-keys [f m]
(->> (map (fn [[k v]] [(f k) v]) m)
(into {})))
(map-keys kebab-case->camelCase
{:character-id 1 :first-name "John" :second-name "Snow"})
=> {:characterId 1, :firstName "John", :secondName "Snow"}
UPDATE: As of Clojure 1.11.0 there is now a built in function in clojure.core
called update-keys
which behaves identically to map-keys
but takes the arguments in the opposite order (update-keys m f)
.
Converting case of keys in a nested data structure
For converting the keys of arbitrarily nested data structures we can use the clojure.walk/postwalk
function. Let's check out the docs.
(doc clojure.walk/postwalk)
=>
-------------------------
clojure.walk/postwalk
([f form])
Performs a depth-first, post-order traversal of form. Calls f on
each sub-form, uses f's return value in place of the original.
Recognizes all Clojure data structures. Consumes seqs as with doall.
Combining clojure.walk/postwalk
with our map-keys
function we can create a transform-keys
function that will take a transformation function and apply it to all keys in a data structure.
(defn transform-keys [t form]
(clojure.walk/postwalk (fn [x] (if (map? x) (map-keys t x) x)) form))
(transform-keys kebab-case->camelCase
[{:character-id 1
:first-name "Olaf"
:second-name "Iondrake"
:items {:bag-of-holding ["sword" "axe" "money"]}}
{:character-Id 2
:first-name "Sigurd"
:second-name "Rockfist"
:items {:bag-of-holding ["scroll" "potion of healing"]}}])
=> [{:characterId 1,
:firstName "Olaf",
:secondName "Irondrake",
:items {:bagOfHolding ["sword" "axe" "money"]}}
{:characterId 2,
:firstName "Sigurd",
:secondName "Rockfist",
:items {:bagOfHolding ["scroll" "potion of healing"]}}]
There you have it, a function for converting the case of keys in an arbitrarily nested data structure. You can use this function at the boundaries of your software stack to keep you and your team sane.
For more robust case conversion checkout the awesome camel-snake-kebab library.