【问题标题】:How to represent non-standard java objects when encoding to JSON in Clojure?在 Clojure 中编码为 JSON 时如何表示非标准 java 对象?
【发布时间】:2020-09-04 23:57:12
【问题描述】:

我有一个标准的 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 库,但是如果你能找到一种方法来完成上述操作,那么我将获得满分。

【问题讨论】:

    标签: json clojure cheshire


    【解决方案1】:

    使用 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"}
    

    【讨论】:

    • 迄今为止最有希望的。我会测试一下这个想法,然后告诉你进展如何,谢谢。
    • 做到了!虽然我会补充一点,但我发现直接调用generate/write-string 而不是.writeString 更简洁:)
    【解决方案2】:

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

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

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

    【讨论】:

      【解决方案3】:

      除非我遗漏了什么,否则这里不需要 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 对象这样的东西无论如何都是不可能的。


      如果您愿意,可以使用相关函数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\"]"}
      

      【讨论】:

      • 我很抱歉没有说清楚,但我正在编写的特定库的所有输出和输入都是通过 json 标准输出和标准输入。该地图实际上是在 json 集中输出的更大地图的一部分,因此可以认为 json 只是构建的要求。
      【解决方案4】:

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

      【讨论】:

        猜你喜欢
        • 2021-04-21
        • 1970-01-01
        • 2021-05-07
        • 2015-12-23
        • 2019-10-17
        • 1970-01-01
        • 2011-12-25
        • 1970-01-01
        相关资源
        最近更新 更多