Liking cljdoc? Tell your friends :D

Serialization

Bankster provides per-format serialization protocols for JSON and EDN, with a clean separation between minimal and full representations.

Namespaces

NamespacePurpose
io.randomseed.bankster.serializers.jsonJSON serialization (strings for amounts, string IDs)
io.randomseed.bankster.serializers.ednEDN serialization (BigDecimals, keyword IDs, tagged literals)

JSON protocols

(defprotocol JsonSerializable
  (to-json-map    [this] [this opts])   ; minimal or full map
  (to-json-full-map [this] [this opts]) ; always full map
  (to-json-string [this] [this opts]))  ; canonical string

(defprotocol JsonDeserializable
  (from-json-map    [type-token m] [type-token m opts])
  (from-json-string [type-token s] [type-token s opts]))

EDN protocols

(defprotocol EdnSerializable
  (to-edn-map     [this] [this opts])   ; minimal or full map
  (to-edn-full-map [this] [this opts])  ; always full map
  (to-edn-string  [this] [this opts]))  ; tagged literal

(defprotocol EdnDeserializable
  (from-edn-map    [type-token m] [type-token m opts])
  (from-edn-string [type-token s] [type-token s opts]))

Minimal vs full serialization

By default, serialization produces minimal output:

TypeJSON minimalEDN minimal
Currency{:id "PLN"}{:id :PLN}
Money{:currency "PLN", :amount "12.30"}{:currency :PLN, :amount 12.30M}

Full serialization (to-*-full-map or :full? true) includes all fields:

TypeJSON fullEDN full
Currency{:id, :numeric, :scale, :kind, :domain}{:id, :numeric, :scale, :kind, :domain}
Money{:currency {...}, :amount} + extended fields{:currency {...}, :amount} + extended fields

Options

KeyTypeContextDescription
:code-only?booleanserOmit namespace: :crypto/ETH"ETH" (JSON) / :ETH (EDN)
:full?booleanserDelegate to-*-map to to-*-full-map
:keysvectorserFilter output keys; supports nested opts for recursive serialization
:rescaleintegerser/deserScale to apply to amounts (see Rescaling section)
:registryRegistrydeserRegistry for deserialization (default: registry/get)
:rounding-modeRoundingMode/keyword/stringser/deserRounding mode for rescaling

Rounding mode formats

The :rounding-mode option accepts multiple formats:

  • java.math.RoundingMode object: RoundingMode/HALF_UP
  • Keyword: :HALF_UP, :HALF_DOWN, :CEILING, etc.
  • String: "HALF_UP", "ROUND_HALF_UP"

When not specified, falls back to scale/*rounding-mode* dynamic var.

Nested :keys filtering

The :keys option supports nested options via map elements:

;; Filter Money to only :amount and currency's :id/:numeric
(to-json-full-map money {:keys [:amount {:currency {:keys [:id :numeric]}}]})
;; => {:amount "12.30", :currency {:id "PLN", :numeric 985}}

Syntax for :keys vector:

  • Keyword elements select that field without recursion
  • Map elements {:field-name {:keys [...]}} select that field with nested options

Extended fields

Money records may carry extended fields (via assoc). Full serialization includes them:

(def m (assoc (money/of :PLN 100) :memo "Payment"))
(to-json-full-map m)
;; => {:currency {...}, :amount "100.00", :memo "Payment"}

Extended fields in JSON are stringified (BigDecimal → .toPlainString, keyword → name, other → str). In EDN they are preserved as-is.

Format semantics

AspectJSONEDN
Currency IDString ("PLN", "crypto/ETH")Keyword (:PLN, :crypto/ETH)
AmountString (.toPlainString)BigDecimal
Tagged literalN/A#money[12.30M PLN], #money/crypto[1.5M ETH]
KindString with namespace ("iso/fiat")Keyword with namespace (:iso/fiat)
DomainString ("ISO-4217")Keyword (:ISO-4217)

Convenience functions

Both namespaces expose helper functions that mirror protocol methods:

;; JSON
(money->json-map m)          ; minimal
(money->json-full-map m)     ; full
(money->json-string m)       ; "12.30 PLN"
(json-map->money m)
(json-string->money s)
(currency->json-map c)       ; minimal {:id "PLN"}
(currency->json-full-map c)  ; full
(currency->json-string c)    ; "PLN"
(json-map->currency m)
(json-string->currency s)

;; EDN
(money->edn-map m)           ; minimal
(money->edn-full-map m)      ; full
(money->edn-string m)        ; "#money[12.30M PLN]"
(edn-map->money m)
(edn-string->money s)
(currency->edn-map c)        ; minimal {:id :PLN}
(currency->edn-full-map c)   ; full
(currency->edn-string c)     ; "#currency :PLN"
(edn-map->currency m)
(edn-string->currency s)
(edn-keyword->currency k)

Cheshire integration (JSON)

(require '[io.randomseed.bankster.serializers.json :as sj])

;; Register Money encoder with Cheshire (map representation)
(sj/register-cheshire-codecs!)

;; Or use string representation
(sj/register-cheshire-codecs! {:representation :string})

;; With :code-only? option
(sj/register-cheshire-codecs! {:code-only? true})

Note: Cheshire registration affects encoding only. Decoding remains explicit via json-map->money / json-string->money.

Codec helpers

Both namespaces provide money-codec for building encode/decode function pairs:

(let [{:keys [encode decode]} (sj/money-codec {:representation :map
                                               :code-only? true
                                               :registry my-reg
                                               :rounding-mode RoundingMode/HALF_UP})]
  (encode money)   ; -> map or string
  (decode data))   ; -> Money

The codec returns a map with:

  • :encode - function (Money -> json-value)
  • :decode - function (json-value -> Money)
  • :representation - :map or :string

Usage examples

Basic serialization

(require '[io.randomseed.bankster.serializers.json :as sj]
         '[io.randomseed.bankster.serializers.edn  :as se])

;; JSON minimal (default)
(sj/money->json-map #money[12.30 PLN])
;; => {:currency "PLN", :amount "12.30"}

;; JSON full
(sj/money->json-full-map #money[12.30 PLN])
;; => {:currency {:id "PLN", :numeric 985, :scale 2, :kind "iso/fiat", :domain "ISO-4217"},
;;     :amount "12.30"}

;; EDN minimal
(se/money->edn-map #money[12.30 PLN])
;; => {:currency :PLN, :amount 12.30M}

;; EDN tagged literal
(se/money->edn-string #money[12.30 PLN])
;; => "#money[12.30M PLN]"

(se/money->edn-string #money/crypto[1.5 ETH])
;; => "#money/crypto[1.500000000000000000M ETH]"

Deserialization

(sj/json-map->money {:currency "PLN" :amount "12.30"})
;; => #money[12.30 PLN]

(sj/json-string->money "12.30 PLN")
;; => #money[12.30 PLN]

(se/edn-string->money "#money[12.30M PLN]")
;; => #money[12.30 PLN]

;; With custom registry and rounding
(sj/json-map->money {:currency "PLN" :amount "1.005"}
                    {:rounding-mode java.math.RoundingMode/HALF_UP})
;; => #money[1.01 PLN]

Using :full? option

;; Via to-json-map with :full? true
(sj/to-json-map #money[12.30 PLN] {:full? true})
;; => {:currency {:id "PLN", ...}, :amount "12.30"}

;; Via protocol
(sj/to-json-map (currency/of :PLN) {:full? true})
;; => {:id "PLN", :numeric 985, :scale 2, :kind "iso/fiat", :domain "ISO-4217"}

Filtering with :keys

;; Select specific fields
(sj/money->json-full-map #money[12.30 PLN] {:keys [:amount {:currency {:keys [:id :numeric]}}]})
;; => {:amount "12.30", :currency {:id "PLN", :numeric 985}}

;; Currency only
(sj/currency->json-full-map (currency/of :PLN) {:keys [:id :scale]})
;; => {:id "PLN", :scale 2}

Code-only mode

;; Omit namespace in currency IDs
(sj/money->json-map #money/crypto[1.5 ETH] {:code-only? true})
;; => {:currency "ETH", :amount "1.500000000000000000"}

(se/money->edn-string #money/crypto[1.5 ETH] {:code-only? true})
;; => "#money[1.500000000000000000M ETH]"

Rescaling

The :rescale option allows serializing/deserializing Money with a different scale than the currency's nominal scale.

Serialization with :rescale

Rescale amounts before outputting:

;; Upscale PLN (scale 2) to 4 decimal places
(sj/money->json-map #money[12.30 PLN] {:rescale 4})
;; => {:currency "PLN", :amount "12.3000"}

;; Downscale requires rounding mode (or throws ArithmeticException)
(sj/money->json-map #money/crypto[1.12345 ETH] {:rescale 2 :rounding-mode :HALF_UP})
;; => {:currency "crypto/ETH", :amount "1.12"}

;; Works with all serialization functions
(sj/money->json-string #money[12.30 PLN] {:rescale 4})
;; => "12.3000 PLN"

Deserialization with :rescale

Preserve precision when the incoming data has more decimal places than the registry currency. Without :rescale, the amount would be truncated to the currency's nominal scale, potentially losing data.

;; PLN has scale 2, but incoming data has 4 decimal places
;; Without :rescale - data loss!
(sj/json-map->money {:currency "PLN" :amount "12.3456"})
;; => ArithmeticException (or truncated if rounding-mode set)

;; With :rescale - precision preserved
(sj/json-map->money {:currency "PLN" :amount "12.3456"} {:rescale 4})
;; => #money[12.3456 PLN] (Currency has scale 4, not the registry's 2)

;; The resulting Money has a Currency with the custom scale
(let [m (sj/json-map->money {:currency "PLN" :amount "12.3456"} {:rescale 4})]
  (.scale (currency/of m)))
;; => 4

How :rescale works in deserialization

  1. Currency is looked up from the registry (by ID)
  2. The Currency object is cloned with the :rescale value as its scale
  3. Money is created with this modified Currency

This means the resulting Money object carries a Currency with a non-standard scale. This is intentional - it preserves all information from the wire format without data loss.

Use cases for :rescale

  • APIs with variable precision: When external systems send amounts with more precision than your currency's default scale
  • Cross-system communication: When systems have different scale conventions
  • Migration scenarios: When converting from one scale to another
  • Display formatting: When you need amounts in a specific format for display

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close