Support both xml and json REST response in clojure

Say I have a REST API in java and it supports responses that are JSON or XML. The answers contain the same data, but the form is not identical. For example, in json, I could:

{ "persons": [ { "name":"Bob", "age":24, "hometown":"New York" } ] } 

While in XML it looks like this:

 <persons> <person name="bob" age="24"> <hometown>New York</hometown> </person> </persons> 

That is, some values ​​are attributes for a person, while others are child attributes. In Java, using JAXB and Jackson, it's easy to hide such differences with annotations on model objects, for example:

 public class Person { @XmlAttribute String name; @XmlAttribute Integer age; @XmlElement String hometown; } 

JAXB reads annotations, and Jackson uses field names to figure out what to do. Thus, with one model it is easy to support several output formats.

So my question is: how to do the same in clojure. I know that there is clj-json that can easily convert clojure maps and vectors to json (using jackson if I'm not mistaken). And I know that there are both clojure.xml.emit and clojure.contrib.xml.prxml that can deserialize maps and vectors into XML. But if I’m not mistaken, I don’t think that these two will work together very well.

Because prxml expects xml nodes to be expressed as vectors, and xml attributes will be expressed as a map, fundamentally different from clj-json, where vectors are arrays and maps represent objects. And clojure.core.emit expects a map of the form {:tag :person :attrs {:name "Bob" :age 24} :content ...} , which is completely different from 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 converts the data structure to what clj-json wants when the response type is JSON. But it looks like the lame. I would prefer if there were a couple of JSON and XML libraries that were compatible with both JAXB and Jackson.

Ideas?

+6
source share
1 answer

Much depends on how you decide to represent the models in your code.

Suppose you are using records. Here's a contrived example of how you can "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 [[kv] 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)) 

Using:

 > (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 

You can create a macro to define a record and its XML attributes in a single expression. For instance.

 (defrecord-xml Person [^:xml-attr name ^:xml-attr age hometown]) 
+5
source

Source: https://habr.com/ru/post/912142/


All Articles