How to convert JSON to EDN in Clojure?

4.5k Views Asked by At

I think this is a simple question but as a beginner in Clojure, I would like to convert a simple JSON to EDN in Clojure.

My JSON:

{
    "Data": [
        {
            "Metadata": {
                "Series": "1/2"
            },
            "Hybrid": {
                "Foo": 76308,
                "Bar": "76308",
                "Cat": "Foo123"
            }
        }
    ],
    "Footer": {
        "Count": 3,
        "Age": 0
    }
}

So if we assume that data is the json above, I have tried to convert it to an EDN using Cheshire like so:

(log/info "data" (cheshire.core/parse-string {(data) true}))

However, whenever I run this bit of code, I get the error message:

ERROR compojure.api.exception - clojure.lang.PersistentArrayMap cannot be cast to java.lang.String

I think I am getting this because my JSON is not a string, but unsure if I first need to convert it to a string, and then converting to EDN - or if there is a way I can go straight from JSON to EDN?

Thank you for your help in advance

3

There are 3 best solutions below

0
On

This will help to clarify what is occurring. We start off by including a helpful library:

(ns tst.demo.core
  (:use tupelo.core tupelo.test)
  (:require
    [tupelo.core :as t]
    [tupelo.string :as str]
    ))

In order to avoid the ambiguity of double-quotes for both a literal string and embedded inside of the string, we write the source JSON using single quotes, then use a helper function str/quotes->double to convert every single-quote in the string into a double-quote. Otherwise, we could have read in the source JSON from a file (instead of having it inline).

(def json-str
  (str/quotes->double
    "{
      'Data': [
               {
                'Metadata': {
                             'Series': '1/2'
                            },
                'Hybrid': {
                           'Foo': 76308,
                           'Bar': '76308',
                           'Cat': 'Foo123'
                          }
               }
              ],
      'Footer': {
                 'Count': 3,
                 'Age': 0
                }
     } "))

We first convert the json string into an EDN data structure (not a string). We then convert the EDN data struction into an EDN string. The output (both println and prn) illustrate the differences:

(dotest
  (let [edn-data (t/json->edn json-str) ; JSON string => EDN data
        edn-str  (pr-str edn-data) ; EDN data => EDN string
        ]
    (newline)
    (println "edn-data =>")
    (spy-pretty edn-data)     ; uses 'prn'

    (newline)
    (println "edn-str (println) =>")
    (println edn-str)

    (newline)
    (println "edn-str (prn) =>")
    (prn edn-str)))

with result:

------------------------------------------
   Clojure 1.10.2-alpha1    Java 14.0.1
------------------------------------------

Testing tst.demo.core

edn-data =>
{:Data
 [{:Metadata {:Series "1/2"},
   :Hybrid {:Foo 76308, :Bar "76308", :Cat "Foo123"}}],
 :Footer {:Count 3, :Age 0}}

edn-str (println) =>
{:Data [{:Metadata {:Series "1/2"}, :Hybrid {:Foo 76308, :Bar "76308", :Cat "Foo123"}}], :Footer {:Count 3, :Age 0}}

edn-str (prn) =>
"{:Data [{:Metadata {:Series \"1/2\"}, :Hybrid {:Foo 76308, :Bar \"76308\", :Cat \"Foo123\"}}], :Footer {:Count 3, :Age 0}}"

Think carefully about what is a data structure and what is a string. If we write [1 :b "hi"] in a Clojure source file, the Clojure Reader creates a data structure that is a 3-element vector containg an int, a keyword, and a string. If we convert that into a string using str, the output is just a string of 11 characters and is not a data structure.

However, if we want to paste that string (inside of double-quotes) into our source file, we need not only the outer double-quotes to mark the beginning and end of the string, but each double-quote inside the string (e.g. as part of the "hi") needs to be escaped, so the Clojure Reader can tell that they belong inside of the string and don't mark the beginning or end of a string.

It takes a while to get used to all of the different modes!


Clojure Reader vs Compiler

Clojure source code files are processed in 2 passes.

  1. The Clojure Reader takes the text of each source file (a giant string of many lines) and converts it into a data structure.

  2. The Clojure Compiler takes the data structure from (1) and outputs Java bytecode.

0
On

You are getting this error because you are passing the variable data as a clojure Map and not as a String (or a json object).

Thats actually what the error says as well.

cheshire.core/parse-string expects a json object which will be a string.

'{ "name": "Cheshire", "needs": "a string"}'
2
On

You get this error for this line:

(cheshire.core/parse-string {(data) true})

What is happening here:

  1. Whatever your data is, it seems to be call-able - or else (data) would already fail. E.g. with ("{}") will give you an class java.lang.String cannot be cast to class clojure.lang.IFn error. Also it's unlikely, that data is already the de-serialized map, as maps are functions, but have an arity of at least one. So we have to assume, that data is actually a function and returns whatever.
  2. Next this result of this call is put into a new map, as key and the value true ({? true}).
  3. Then this map is passed to cheshire's parse-string, but this clearly is no string, but a map, hence the error.

Given all that, and assuming, that (data) returns a String, your best bet is:

(cheshire.core/parse-string (data))

And if you really want the EDN for that, that would be:

(pr-str (cheshire.core/parse-string (data)))