Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

support both xml and json REST response in clojure

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?

like image 848
Kevin Avatar asked Mar 31 '12 20:03

Kevin


1 Answers

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])
like image 52
Justin Kramer Avatar answered Sep 22 '22 04:09

Justin Kramer