Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to transform object keys and values in Clojure

I'm using Datomic, although it doesn't particularly matter for this question. But it typically returns namespaced keys (and enum values are returned as namespaces keywords also). I want to translate the potentially nested structure to strip the namespaces from the keys and from values (and also string-ify the values of enums). I'm doing this because I'll return the result in a JSON REST API and the namespacing doesn't make much sense in that context. Here's a simple example structure:

{ 
    :person/name "Kevin"
    :person/age 99
    :person/gender :gender/M
    :person/address {
        :address/state :state/NY
        :address/city "New York"
        :address/zip "99999"
    }
}

And I'm hoping to translate to:

{ 
    :name "Kevin"
    :age 99
    :gender "M"
    :address {
        :state "NY"
        :city "New York"
        :zip "99999"
    }
}

One thing I know I can do is use (postwalk-replace {:person/name :name :person/age :age :person/gender :gender :person/address :address :address/city :city :address/state :state :address/zip :zip} the-entity) and that covers the keys, but not the values.

What other options do I have?

like image 486
Kevin Avatar asked Aug 10 '26 05:08

Kevin


2 Answers

You can use clojure.walk/postwalk. Simple version doesn't differentiate between keywords as keys or values in a map, simply converts all keys to strings:

(def data {:person/name "Kevin"
           :person/age 99
           :person/gender :gender/M
           :person/address {:address/state :state/NY
                            :address/city "New York"
                            :address/zip "99999"}})


(clojure.walk/postwalk
  (fn [x]
    (if (keyword? x)
      (name x)
      x))
  data)

;; => => {"name" "Kevin", "age" 99, "gender" "M", "address" {"state" "NY", "city" "New York", "zip" "99999"}}

To implement exactly what you want you need to handle keys and values in a map separately:

(defn transform-keywords [m]
  (into {}
        (map (fn [[k v]]
               (let [k (if (keyword? k) (keyword (name k)) k)
                     v (if (keyword? v) (name v) v)]
                 [k v]))
             m)))

(clojure.walk/postwalk
  (fn [x]
    (if (map? x)
      (transform-keywords x)
      x))
  data)

;; => => {:name "Kevin", :age 99, :gender "M", :address {:state "NY", :city "New York", :zip "99999"}}
like image 117
Piotrek Bzdyl Avatar answered Aug 13 '26 17:08

Piotrek Bzdyl


As a side note: in my experience, the impedance mismatch between namespace-qualified and non-namespace-qualified keys at the boundary of your system can be an ongoing pain; what's more, having namespaced-qualified keys has significant advantages regarding code clarity (very good data traceability).

So I wouldn't give up namespace-qualified keys too readily. If EDN's syntax for namespacing (with dots and slashes) doesn't suit the consumers of your API, you may even want to use something more conventional like underscores (e.g :person_name instead of :person/name); a bit uglier, but it still gives you most of the benefits of namespace-qualified keys, you will not even need to transform the data structures, and Datomic won't mind.

like image 22
Valentin Waeselynck Avatar answered Aug 13 '26 19:08

Valentin Waeselynck



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!