Как представить нестандартные объекты java при кодировании в JSON в 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]" }

Я хочу сделать это, потому что моя программа представляет собой библиотеку синтаксического анализа интерфейса командной строки, и я работаю с вызывающей стороной библиотеки, чтобы построить эту карту, поэтому я точно не знаю, какие типы данных будут в ней. Тем не менее, я все же хотел бы распечатать его на экране, чтобы помочь вызывающему абоненту в отладке. Я попытался наивно передать эту карту чеширу, но когда я это сделал, чешир продолжает задыхаться от этой ошибки:

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

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

4 ответа

Решение

С помощью чешира вы можете добавить кодировщик для 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"}

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

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

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

Отлично читается.

Как сказал Денис, вам понадобится больше работы, если вы захотите прочитать данные, но это невозможно для таких вещей, как 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\"]"}
Другие вопросы по тегам