Say I have a REST API in java, and it supports responses that are either JSON or XML. The responses contain the same data, but the form is not identical. For example, in json I might have:
{
"persons": [
{
"name":"Bob",
"age":24,
"hometown":"New York"
}
]
}
Whereas in XML it looks like this:
<persons>
<person name="bob" age="24">
<hometown>New York</hometown>
</person>
</persons>
Which is to say that some values are attributes on person, and others are child elements. In Java, using JAXB and Jackson, it's easy to hide such differences with annotations on the model objects, for example:
public class Person {
@XmlAttribute
String name;
@XmlAttribute
Integer age;
@XmlElement
String hometown;
}
JAXB reads the annotations, and Jackson uses the field names to figure out what to do. So with a single model it's easy to support multiple output formats.
So my question is, how to do the same thing in clojure. I know that there is clj-json which can easily convert clojure maps and vectors to json (using jackson if I'm not mistaken). And I know there is both clojure.xml.emit and clojure.contrib.xml.prxml that can deserialize maps & vectors to XML. But unless I'm mistaken, I don't think these two would work together very well.
Because prxml expects xml nodes to be expressed as vectors, and xml attributes to be expressed as a map, fundamentally different than the working of clj-json, where vectors represent arrays, and maps represent objects. And clojure.core.emit expects a map in the form {:tag :person :attrs {:name "Bob" :age 24} :content ...}
which is again completely different than what clj-json wants.
The only thing I can think of is to format the data structures for prxml in my code, and then write a function that transforms the data structure to what clj-json wants when the response type is JSON. But that seems kind of lame. I would prefer if there was a pair of JSON and XML libraries that were compatible in the way that JAXB and Jackson are.
Ideas?
A lot depends on how you choose to represent models in your code.
Let's assume you use records. Here's a contrived example of how you might "annotate" a record and provide serializers for XML and JSON.
;; Depends on cheshire and data.xml
(ns user
(:require [cheshire.core :as json]
[clojure.data.xml :as xml]))
(defrecord Person [name age hometown])
(defrecord Animal [name sound])
(def xml-attrs {Person [:name :age]
Animal [:name]})
(defn record->xml-data [rec]
(let [tag (-> rec class .getSimpleName .toLowerCase keyword)
attrs (select-keys rec (xml-attrs (class rec)))
content (for [[k v] rec
:when (not (contains? attrs k))]
(xml/element k nil (str v)))]
(apply xml/element tag attrs content)))
(defn record->xml [rec]
(xml/emit-str (record->xml-data rec)))
(defn record->json [rec]
(json/generate-string rec))
Usage:
> (def bob (Person. "Bob" 24 "New York"))
#'user/bob
> (println (record->xml bob))
<?xml version="1.0" encoding="UTF-8"?><person age="24" name="Bob"><hometown>New York</hometown></person>
nil
> (println (record->json bob))
{"name":"Bob","age":24,"hometown":"New York"}
nil
> (println (record->xml (Animal. "Fido" "Bark!")))
<?xml version="1.0" encoding="UTF-8"?><animal name="Fido"><sound>Bark!</sound></animal>
nil
A macro could be created to define a record and its XML attributes in a singe statement. E.g.,
(defrecord-xml Person [^:xml-attr name ^:xml-attr age hometown])
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With