0

我有一个标准的clojure地图。键是关键字,值是任意值。它们可以是nil、数字、字符串或任何其他类型的 JVM 对象/类。

我需要知道如何将此映射编码为 JSON,以便“正常”值映射到通常的 JSON 值(例如关键字 -> 字符串、整数 -> JSON 数字等),而任何其他类的值映射到这些值的字符串表示形式,如下所示:

{
  :a 1
  :b :myword
  :c "hey"
  :d <this is an "unprintable" java File object>
}

如此编码:

{ "a": 1, "b": "myword", "c": "hey", "d": "#object[java.io.File 0x6944e53e foo]" }

我想这样做是因为我的程序是一个 CLI 解析库,并且我正在与库的调用者一起构建这个映射,所以我不完全知道其中将包含哪些类型的数据。但是,我想将它打印到屏幕上,以帮助调用者进行调试。我曾试图天真地将这张地图提供给 cheshire,但是当我这样做时,cheshire 一直因这个错误而窒息:

Exception in thread "main" com.fasterxml.jackson.core.JsonGenerationException: Cannot JSON encode object of class: class java.io.File: foo

奖励:我正在努力减少我的依赖计数,并且我已经将cheshire作为我的 JSON 库进行了审查,但是如果你能找到一种方法在没有它的情况下执行上述操作,则满分。

4

4 回答 4

2

使用 cheshire,您可以为 java.lang.Object 添加编码器

user> (require ['cheshire.core :as 'cheshire])
nil

user> (require ['cheshire.generate :as 'generate])
nil

user> (generate/add-encoder Object (fn [obj jsonGenerator] (.writeString jsonGenerator (str obj))))
nil

user> (def json (cheshire/generate-string {:a 1 :b nil :c "hello" :d (java.io.File. "/tmp")}))
#'user/json

user> (println json)
{"a":1,"b":null,"c":"hello","d":"/tmp"}
于 2020-05-19T03:11:13.237 回答
1

您还可以覆盖print-method您感兴趣的某些对象:

(defmethod print-method java.io.File [^java.io.File f ^java.io.Writer w]
  (print-simple (str "\"File:" (.getCanonicalPath f) "\"") w))

每次需要打印出这种类型的对象时,打印子系统都会调用该方法:

user> {:a 10 :b (java.io.File. ".")}

;;=> {:a 10,
;;    :b "File:/home/xxxx/dev/projects/clj"}
于 2020-05-19T08:22:56.340 回答
0

Cheshire 包含自定义编码器,您可以创建和注册以序列化任意类。

OTOH,如果您想读回 JSON 并在 Java 中重现相同的类型,您还需要添加一些元数据。一种常见的模式是将类型编码为类似__typeor的字段*class*,这样反序列化器可以找到正确的类型:

{
  __type: "org.foo.User"
  name: "Jane Foo"
  ...
}
于 2020-05-19T03:18:31.240 回答
0

除非我遗漏了什么,否则这里不需要 JSON。只需使用prn

(let [file (java.io.File. "/tmp/foo.txt")]
    (prn {:a 1 :b "foo" :f file})

=> {:a 1, 
    :b "foo", 
    :f #object[java.io.File 0x5d617472 "/tmp/foo.txt"]}

完全可读。

正如丹尼斯所说,如果你想读回数据,你需要做更多的工作,但这对于像File对象这样的东西来说是不可能的。


如果您愿意,可以println使用相关函数将结果作为字符串(适用于等)获取:pretty-str

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

(dotest
  (let [file (java.io.File. "/tmp/foo.txt")]
    (println (t/pretty-str {:a 1 :b "foo" :f file}))
    ))

=> {:a 1, 
    :b "foo", 
    :f #object[java.io.File 0x17d96ed9 "/tmp/foo.txt"]}

更新

这是我需要将数据强制转换为另一种形式时经常使用的技术,尤其是。用于调试或单元测试:

(ns tst.demo.core
  (:use tupelo.core tupelo.test)
  (:require
    [clojure.walk :as walk]))

(defn walk-coerce-jsonable
  [edn-data]
  (let [coerce-jsonable (fn [item]
                          (cond
                            ; coerce collections to simplest form
                            (sequential? item) (vec item)
                            (map? item) (into {} item)
                            (set? item) (into #{} item)

                            ; coerce leaf values to String if can't JSON them
                            :else (try
                                    (edn->json item)
                                    item ; return item if no exception
                                    (catch Exception ex
                                      (pr-str item))))) ; if exception, return string version of item
        result          (walk/postwalk coerce-jsonable edn-data)]
    result))

(dotest
  (let [file (java.io.File. "/tmp/foo.txt")
        m    {:a 1 :b "foo" :f file}]
    (println :converted (edn->json (walk-coerce-jsonable m)))
    ))

结果

-------------------------------
   Clojure 1.10.1    Java 14
-------------------------------

Testing tst.demo.core
:converted {"a":1,"b":"foo","f":"#object[java.io.File 0x40429f12 \"/tmp/foo.txt\"]"}
于 2020-05-19T04:21:53.263 回答