在 Clojure 中编码为 JSON 时如何表示非标准 java 对象?

How to represent non-standard java objects when encoding to JSON in Clojure?

我有一个标准的 clojure 事物映射。键是关键字,值是任意值。它们可以是 nil、数字、字符串或任何其他类型的 JVM object/class.

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

{
  :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 解析库,我正在与库的调用者一起构建这个映射,所以我不完全知道里面会有什么类型的数据.但是,我还是想将它打印到屏幕上以帮助调用者进行调试。我试图天真地把这张地图交给柴郡,但当我这样做时,柴郡一直被这个错误所困扰:

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

奖金:我正在努力减少我的依赖计数并且我已经审查了 cheshire 作为我的 JSON 库,但是如果你能找到一种没有它的方法来完成上述操作,则满分.

使用柴郡,您可以为 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"}

Cheshire 包括 Custom Encoders,您可以创建和注册以序列化任意 类。

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

{
  __type: "org.foo.User"
  name: "Jane Foo"
  ...
}

除非我遗漏了什么,否则这里不需要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"]}

完全可读。

正如 Denis 所说,如果你想读回数据,你将需要做更多的工作,但无论如何这对于 File 对象这样的东西是不可能的。


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

(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\"]"}

您还可以覆盖您感兴趣的某些对象的 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"}