Как представить нестандартные объекты Java при кодировании в JSON в Clojure?

У меня есть стандартная карта вещей 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, и я работаю с вызывающей стороной библиотеки, чтобы создать эту карту, поэтому я точно не знаю, какие типы данных будут в ней. Тем не менее, я хотел бы все равно вывести его на экран, чтобы помочь вызывающему абоненту в отладке. Я пытался наивно отдать эту карту Чеширу, но когда я это делаю, Чешир продолжает задыхаться с этой ошибкой:

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

Бонус: я пытаюсь снизить количество зависимостей и уже проверил cheshire в качестве моей библиотеки JSON, но полный балл, если вы можете найти способ сделать это без него.


person djhaskin987    schedule 19.05.2020    source источник


Ответы (4)


С помощью 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"}
person jas    schedule 19.05.2020
comment
Самый перспективный на данный момент. Я проверю идею и дам вам знать, как она работает, спасибо. - person djhaskin987; 19.05.2020
comment
Это сделало это! хотя я добавлю, что мне показалось чище вызывать generate/write-string, а не .writeString напрямую :) - person djhaskin987; 19.05.2020

вы также можете переопределить 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"}
person leetwinski    schedule 19.05.2020

Cheshire включает пользовательские кодировщики, которые можно создавать и регистрировать для сериализации произвольных классов.

OTOH, если вы хотите прочитать JSON и воспроизвести те же типы обратно в Java, вам также нужно будет добавить некоторые метаданные. Распространенным шаблоном является кодирование типа как некоторого поля, такого как __type или *class*, например, так, чтобы десериализатор мог найти правильные типы:

{
  __type: "org.foo.User"
  name: "Jane Foo"
  ...
}
person Denis Fuenzalida    schedule 19.05.2020

Если я что-то не упустил, 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\"]"}
person Alan Thompson    schedule 19.05.2020
comment
Я прошу прощения за то, что не разъяснил это, но все выходные и входные данные для конкретной библиотеки, которую я пишу, стандартизированы и стандартизированы через json. Карта на самом деле будет частью большей карты, которая выводится в наборе json, поэтому можно считать, что json — это просто требование сборки. - person djhaskin987; 19.05.2020