diff --git a/README.md b/README.md
index 23dee1a..8bfd014 100644
--- a/README.md
+++ b/README.md
@@ -5,13 +5,15 @@ datatype modules to support 3rd party libraries.
Currently included are:
+* [jackson-datatype-jakarta-mail](jakarta-mail/) for Jakarta Mail (ex-Java Mail) (starting with Jackson 2.13)
+ * Currently (2.13) just type `jakarta.mail.internet.InternetAddress`
+* [jackson-datatype-javax-money](javax-money/) for [JSR 354](https://github.com/JavaMoney/jsr354-api) datatypes (starting with Jackson 2.19)
+* [jackson-datatype-moneta](moneta/) for [JavaMoney Moneta RI](https://javamoney.github.io/) datatypes (jsr354 reference implementation) (starting with Jackson 2.19)
* [jackson-datatype-joda-money](joda-money/) for [Joda-Money](https://www.joda.org/joda-money/) datatypes
* JSR-353/JSON-P: 2 variants (starting with Jackson 2.12.2)
* [jackson-datatype-jsr353](jsr-353/) for older "javax.json" [JSR-353](https://www.jcp.org/en/jsr/detail?id=353) (aka JSON-P) datatypes (package `javax.json`)
* [jackson-datatype-jakarta-jsonp](jakarta-jsonp/) for newer "Jakarta" JSON-P datatypes (package `jakarta.json`)
* [jackson-datatype-json-org](json-org/) for ([org.json](http://json.org/java)) JSON model datatypes (included in Android SDK, as well as stand-alone Java library)
-* [jackson-datatype-jakarta-mail](jakarta-mail/) for Jakarta Mail (ex-Java Mail) (starting with Jackson 2.13)
- * Currently (2.13) just type `jakarta.mail.internet.InternetAddress`
Note that this repo was created for Jackson 2.11: prior to this, individual datatype
modules had their own repositories.
@@ -67,6 +69,9 @@ ObjectMapper mapper = JsonMapper.builder()
.addModule(new JsonOrgModule())
.addModule(new JodaMoneyModule())
// ONE of these (not both):
+ .addModule(new JavaxMoneyModule())
+ .addModule(new MonetaMoneyModule())
+ // ONE of these (not both):
.addModule(new JSR353Module()) // old (javax) json-p API
.addModule(new JSONPModule()) // new (jakarta) json-P API
.build();
diff --git a/javax-money/MONEY.md b/javax-money/MONEY.md
new file mode 100644
index 0000000..7070876
--- /dev/null
+++ b/javax-money/MONEY.md
@@ -0,0 +1,118 @@
+# Representing Money in JSON
+
+> A large proportion of the computers in this world manipulate money, so it's always puzzled me that money isn't actually a first class data type in any mainstream programming language.
+>
+> [Martin Fowler](https://martinfowler.com/eaaCatalog/money.html)
+
+Unfortunately JSON is no different. This document tries to change that by proposing and comparing different styles to represent money, some inspired by external sources and some based on our own experience.
+
+## ⚠️ Monetary amounts ≠ floats
+
+Before we dive into details, always keep the following in mind. However you desire to format money in JSON, nothing changes the fact that you should...
+
+> **Never hold monetary values [..] in a float variable.** Floating point is not suitable for this work, and you must use either [fixed-point](#fixed-point) or [decimal](#decimal) values.
+>
+> [Coinkite: Common Terms and Data Objects](https://web.archive.org/web/20150924073850/https://docs.coinkite.com/api/common.html)
+
+## Styles
+
+We identified the following styles that all of different advantages and disadvantages that are discussed in their respective section.
+
+| Style | Expressive | Arithmetic | Pitfalls / Misuses |
+|------------------------------------|------------|------------|--------------------|
+| [Decimal](#decimal) | ✔ | ✔ | Precision |
+| [Quoted Decimal](#quoted-decimal) | ✔ | ✘ | Parsing |
+| [Fixed Point](#fixed-point) | ✘ | ✔ | Mixed scales |
+| [Mixed](#mixed) | ✘ | ✔ | Consistency |
+
+### Decimal
+
+The most straightforward way to represent a monetary amount would be a base-10 decimal number:
+
+```json
+{
+ "amount": 49.95,
+ "currency": "EUR"
+}
+```
+
+It's expressive, readable and allows arithmetic operations. The downside is that most [JSON decoders will treat it as a floating point](https://tools.ietf.org/html/rfc7159#section-6) number which is very much undesirable.
+
+Most programming languages have support for arbitrary-precision [decimals](#decimal-implementations) and JSON decoders that can be configured to use them. In general it can be considered to be a problem of the implementation, not the format itself.
+
+### Quoted Decimal
+
+Same as [Decimal](#decimal) but quoted so your JSON decoder treats it as a string:
+
+```json
+{
+ "amount": "49.95",
+ "currency": "EUR"
+}
+```
+
+It solves the precision problem of decimals on the expense of performing arithmetic operations on it. It also requires a two-phase parsing, i.e. parsing the JSON text into a data structure and then parsing quoted amounts into decimals.
+
+### Fixed Point
+
+> A value of a fixed-point data type is essentially an integer that is scaled by an implicit specific factor determined by the type.
+>
+> [Wikipedia: Fixed-point arithmetic](https://en.wikipedia.org/wiki/Fixed-point_arithmetic)
+
+```json
+{
+ "amount": 4995,
+ "currency": "EUR"
+}
+```
+
+The implicit scaling factor is defined as (0.1 raised to the power of) the currency's [default number of fraction digits](http://www.localeplanet.com/icu/currency.html).
+
+In rare cases one might need a higher precision, e.g. to have sub-cent. In this case the scale can be defined explicitly:
+
+```json
+{
+ "amount": 499599,
+ "currency": "EUR",
+ "scale": 4
+}
+```
+
+The downside with fixed-point amounts is that reading them is a bit harder and arithmetic with mixed scale amounts can be tricky and error-prone.
+
+### Mixed
+
+As a way to counter all negative aspects of the styles above one idea would be to have a single object that contains all of the formats:
+
+```json
+{
+ "decimal": 49.95,
+ "quoted_decimal": "49.95",
+ "fixed_point": 4995,
+ "scale": 2,
+ "currency": "EUR"
+}
+```
+
+Decoders can choose the representation that fits them the best. Encoders on the other hand have the harder task by providing all of them and making sure that all values are in fact consistent.
+
+## Decimal Implementations
+
+| Language | Implementation |
+|------------|---------------------------------------------------------------------------------------------|
+| C# | [decimal](https://msdn.microsoft.com/en-us/library/364x0z75.aspx) |
+| Java | [java.math.BigDecimal](https://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.html) |
+| JavaScript | [decimal.js](https://github.com/MikeMcl/decimal.js/) |
+| Python | [decimal.Decimal](https://docs.python.org/2/library/decimal.html) |
+
+## Credits and References
+
+- [Coinkite: Currency Amounts](https://web.archive.org/web/20150924073850/https://docs.coinkite.com/api/common.html#currency-amounts)
+- [Culttt: How to handle money and currency in web applications](http://culttt.com/2014/05/28/handle-money-currency-web-applications/)
+- [Currency codes - ISO 4217](https://www.iso.org/iso-4217-currency-codes.html)
+- [LocalePlanet: ICU Currencies](http://www.localeplanet.com/icu/currency.html)
+- [RFC 7159: The JavaScript Object Notation (JSON) Data Interchange Format](https://tools.ietf.org/html/rfc7159#section-6)
+- [Stackoverflow: What is the standard for formatting currency values in JSON?](http://stackoverflow.com/questions/30249406/what-is-the-standard-for-formatting-currency-values-in-json)
+- [Stackoverflow: Why not use Double or Float to represent currency?](http://stackoverflow.com/questions/3730019/why-not-use-double-or-float-to-represent-currency/3730040#3730040)
+- [TechEmpower: Mangling JSON numbers](https://www.techempower.com/blog/2016/07/05/mangling-json-numbers/)
+- [Wikipedia: Fixed-point arithmetic](https://en.wikipedia.org/wiki/Fixed-point_arithmetic)
diff --git a/javax-money/README.md b/javax-money/README.md
new file mode 100644
index 0000000..08ea3ee
--- /dev/null
+++ b/javax-money/README.md
@@ -0,0 +1,207 @@
+# Jackson Datatype Javax-Money
+
+*Jackson Datatype Javax-Money* is a [Jackson](https://github.com/FasterXML/jackson) module to support JSON serialization and deserialization of [JSR 354](https://github.com/JavaMoney/jsr354-api) data types.
+It fills a niche, in that it integrates [MonetaryAmount](https://javamoney.github.io/apidocs/javax/money/MonetaryAmount.html) and Jackson so that they work seamlessly together, without requiring additional
+developer effort. In doing so, it aims to perform a small but repetitive task — once and for all.
+
+This library reflects an opinionated API [representation of monetary amounts in JSON](MONEY.md)
+
+With this library, it is possible to represent monetary amounts in JSON as follows:
+
+```json
+{
+ "amount": 29.95,
+ "currency": "EUR"
+}
+```
+
+## Features
+
+- enables you to express monetary amounts in JSON
+- can be used in a REST APIs
+- customized field names
+- localization of formatted monetary amounts
+- allows you to implement RESTful API endpoints that format monetary amounts based on the Accept-Language header
+- is unique and flexible
+
+## Dependencies
+
+- Java 8 or higher
+- Any build tool using Maven Central, or direct download
+- Jackson
+
+## Installation
+
+Add the following dependency to your project:
+
+```xml
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-javax-money
+ ${jackson-datatype-money.version}
+
+```
+
+For ultimate flexibility, this module is compatible with any implementation of JSR 354 MonetaryAmount
+
+## Configuration
+
+Register the module with your `ObjectMapper`:
+
+```java
+ObjectMapper mapper = JsonMapper.builder()
+ .addModule(new JavaxMoneyModule())
+ .build();
+```
+
+Alternatively, you can use the SPI capabilities:
+
+```java
+ObjectMapper mapper = new ObjectMapper()
+ .findAndRegisterModules();
+```
+
+### Serialization
+
+For serialization this module currently supports
+[
+`javax.money.MonetaryAmount`](https://github.com/JavaMoney/jsr354-api/blob/master/src/main/java/javax/money/MonetaryAmount.java)
+and will, by default, serialize it as:
+
+```json
+{
+ "amount": 99.95,
+ "currency": "EUR"
+}
+```
+
+To serialize number as a JSON string, you have to configure the quoted decimal number value serializer:
+
+```java
+ObjectMapper mapper = JsonMapper.builder()
+ .addModule(new JavaxMoneyModule().withQuotedDecimalNumbers())
+ .build();
+```
+
+```json
+{
+ "amount": "99.95",
+ "currency": "EUR"
+}
+```
+
+### Formatting
+
+A special feature for serializing monetary amounts is *formatting*, which is **disabled by default**. To enable it, you
+have to either enable default formatting:
+
+```java
+ObjectMapper mapper = JsonMapper.builder()
+ .addModule(new JavaxMoneyModule().withDefaultFormatting())
+ .build();
+```
+
+... or pass in a `MonetaryAmountFormatFactory` implementation to the `JavaxMoneyModule`:
+
+```java
+ObjectMapper mapper = JsonMapper.builder()
+ .addModule(new JavaxMoneyModule()
+ .withFormatting(new CustomMonetaryAmountFormatFactory()))
+ .build();
+```
+
+The default formatting delegates directly to `MonetaryFormats.getAmountFormat(Locale, String...)`.
+
+Formatting only affects the serialization and can be customized based on the *current* locale, as defined by the
+[
+`SerializationConfig`](https://fasterxml.github.io/jackson-databind/javadoc/2.0.0/com/fasterxml/jackson/databind/SerializationConfig.html#with\(java.util.Locale\)).
+This allows to implement RESTful API endpoints
+that format monetary amounts based on the `Accept-Language` header.
+
+The first example serializes a monetary amount using the `de_DE` locale:
+
+```java
+ObjectWriter writer = mapper.writer().with(Locale.GERMANY);
+writer.writeValueAsString(Money.of(29.95, "EUR"));
+```
+
+```json
+{
+ "amount": 29.95,
+ "currency": "EUR",
+ "formatted": "29,95 EUR"
+}
+```
+
+The following example uses `en_US`:
+
+```java
+ObjectWriter writer = mapper.writer().with(Locale.US);
+writer.writeValueAsString(Money.of(29.95, "USD"));
+```
+
+```json
+{
+ "amount": 29.95,
+ "currency": "USD",
+ "formatted": "USD29.95"
+}
+```
+
+More sophisticated formatting rules can be supported by implementing `MonetaryAmountFormatFactory` directly.
+
+### Deserialization
+
+This module will not have a default deserialization feature.
+At the same time, if the [Moneta](https://javamoney.github.io/ri.html) library is found in the class path, the module will use `org.javamoney.moneta.Money` as an implementation for `javax.money.MonetaryAmount` by default when deserializing monetary amounts.
+
+Alternatively, in order to deserialize money values, one has to configure the module to use a specific implementation of `javax.money.MonetaryAmount`.
+This can be done by passing the required `MonetaryAmountFactory` to the `JavaxMoneyModule`:
+
+```java
+ObjectMapper mapper = JsonMapper.builder()
+ .addModule(new JavaxMoneyModule()
+ .withMonetaryAmountFactory(new CustomMonetaryAmountFactory()))
+ .build();
+```
+
+You can also pass in a method reference:
+
+```java
+ObjectMapper mapper = JsonMapper.builder()
+ .addModule(new JavaxMoneyModule()
+ .withMonetaryAmountFactory(FastMoney::of))
+ .build();
+```
+
+Please note that, for Moneta implementations like Money, FastMoney and RoundedMoney, the sibling module `jackson-datatype-moneta` can also be used.
+Refer to [javax-money-moneta](../javax-money-moneta/README.md) for more information.
+
+### Custom Field Names
+
+As you have seen in the previous examples the `JavaxMoneyModule` uses the field names `amount`, `currency` and `formatted`
+by default. Those names can be overridden if desired:
+
+```java
+ObjectMapper mapper = JsonMapper.builder()
+ .addModule(new JavaxMoneyModule()
+ .withAmountFieldName("value")
+ .withCurrencyFieldName("unit")
+ .withFormattedFieldName("pretty"))
+ .build();
+```
+
+## Usage
+
+After registering and configuring the module you're now free to directly use `MonetaryAmount` in your data types:
+
+```java
+import javax.money.MonetaryAmount;
+
+public class Product {
+ private String sku;
+ private MonetaryAmount price;
+ ...
+}
+```
\ No newline at end of file
diff --git a/javax-money/pom.xml b/javax-money/pom.xml
new file mode 100644
index 0000000..0d58738
--- /dev/null
+++ b/javax-money/pom.xml
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+ 4.0.0
+
+ com.fasterxml.jackson.datatype
+ jackson-datatypes-misc-parent
+ 2.19.0-SNAPSHOT
+
+ jackson-datatype-javax-money
+ Jackson datatype: javax-money
+ jar
+ 2.19.0-SNAPSHOT
+ Support for datatypes of Money API spec from JSR 354 (https://javamoney.github.io/api.html)
+
+ https://github.com/FasterXML/jackson-datatypes-misc
+
+
+ MIT License
+ https://opensource.org/licenses/MIT
+ repo
+
+
+
+
+ com/fasterxml/jackson/datatype/javax/money
+ ${project.groupId}.javax.money
+ 2.0.6
+
+
+
+
+ javax.money
+ money-api
+ 1.1
+
+
+
+ org.apiguardian
+ apiguardian-api
+ 1.1.2
+ provided
+
+
+ com.google.code.findbugs
+ jsr305
+ 3.0.2
+ provided
+
+
+ org.projectlombok
+ lombok
+ 1.18.34
+ provided
+
+
+
+
+ org.slf4j
+ slf4j-nop
+ ${slf4j.version}
+ test
+
+
+ org.mockito
+ mockito-core
+ 4.5.1
+ test
+
+
+ com.kjetland
+ mbknor-jackson-jsonschema_2.12
+ 1.0.39
+ test
+
+
+ javax.validation
+ validation-api
+ 2.0.1.Final
+ test
+
+
+ org.javamoney
+ moneta
+ 1.4.4
+ pom
+ test
+
+
+
+
+
+
+ com.google.code.maven-replacer-plugin
+ replacer
+
+
+ org.moditect
+ moditect-maven-plugin
+
+
+
+
+
diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/AmountWriter.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/AmountWriter.java
new file mode 100644
index 0000000..b83a36d
--- /dev/null
+++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/AmountWriter.java
@@ -0,0 +1,16 @@
+package com.fasterxml.jackson.datatype.javax.money;
+
+import org.apiguardian.api.API;
+
+import javax.money.MonetaryAmount;
+
+import static org.apiguardian.api.API.Status.EXPERIMENTAL;
+
+@API(status = EXPERIMENTAL)
+public interface AmountWriter {
+
+ Class getType();
+
+ T write(MonetaryAmount amount);
+
+}
diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/BigDecimalAmountWriter.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/BigDecimalAmountWriter.java
new file mode 100644
index 0000000..032f27f
--- /dev/null
+++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/BigDecimalAmountWriter.java
@@ -0,0 +1,17 @@
+package com.fasterxml.jackson.datatype.javax.money;
+
+import org.apiguardian.api.API;
+
+import java.math.BigDecimal;
+
+import static org.apiguardian.api.API.Status.EXPERIMENTAL;
+
+@API(status = EXPERIMENTAL)
+public interface BigDecimalAmountWriter extends AmountWriter {
+
+ @Override
+ default Class getType() {
+ return BigDecimal.class;
+ }
+
+}
diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/CurrencyUnitDeserializer.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/CurrencyUnitDeserializer.java
new file mode 100644
index 0000000..657a7ec
--- /dev/null
+++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/CurrencyUnitDeserializer.java
@@ -0,0 +1,37 @@
+package com.fasterxml.jackson.datatype.javax.money;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer;
+import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
+import org.apiguardian.api.API;
+
+import javax.money.CurrencyUnit;
+import javax.money.Monetary;
+import java.io.IOException;
+
+import static org.apiguardian.api.API.Status.MAINTAINED;
+
+@API(status = MAINTAINED)
+public final class CurrencyUnitDeserializer extends StdScalarDeserializer
+{
+ private static final long serialVersionUID = 1L;
+
+ public CurrencyUnitDeserializer() {
+ super(CurrencyUnit.class);
+ }
+
+ @Override
+ public Object deserializeWithType(final JsonParser parser, final DeserializationContext context,
+ final TypeDeserializer deserializer) throws IOException {
+
+ // effectively assuming no type information at all
+ return deserialize(parser, context);
+ }
+
+ @Override
+ public CurrencyUnit deserialize(final JsonParser parser, final DeserializationContext context) throws IOException {
+ final String currencyCode = parser.getValueAsString();
+ return Monetary.getCurrency(currencyCode);
+ }
+}
diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/CurrencyUnitSerializer.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/CurrencyUnitSerializer.java
new file mode 100644
index 0000000..12a988e
--- /dev/null
+++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/CurrencyUnitSerializer.java
@@ -0,0 +1,36 @@
+package com.fasterxml.jackson.datatype.javax.money;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
+import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+import org.apiguardian.api.API;
+
+import javax.money.CurrencyUnit;
+import java.io.IOException;
+
+import static org.apiguardian.api.API.Status.MAINTAINED;
+
+@API(status = MAINTAINED)
+public final class CurrencyUnitSerializer extends StdScalarSerializer {
+
+ CurrencyUnitSerializer() {
+ super(CurrencyUnit.class);
+ }
+
+ @Override
+ public void serialize(final CurrencyUnit value, final JsonGenerator generator, final SerializerProvider serializers)
+ throws IOException {
+ generator.writeString(value.getCurrencyCode());
+ }
+
+ @Override
+ public void acceptJsonFormatVisitor(final JsonFormatVisitorWrapper visitor, final JavaType hint)
+ throws JsonMappingException {
+ visitor.expectStringFormat(hint);
+ }
+
+}
diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/DecimalAmountWriter.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/DecimalAmountWriter.java
new file mode 100644
index 0000000..734d49f
--- /dev/null
+++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/DecimalAmountWriter.java
@@ -0,0 +1,19 @@
+package com.fasterxml.jackson.datatype.javax.money;
+
+import javax.annotation.Nonnull;
+import javax.money.MonetaryAmount;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+final class DecimalAmountWriter implements BigDecimalAmountWriter {
+
+ @Override
+ public BigDecimal write(@Nonnull final MonetaryAmount amount) {
+ final BigDecimal decimal = amount.getNumber().numberValueExact(BigDecimal.class);
+ final int defaultFractionDigits = amount.getCurrency().getDefaultFractionDigits();
+ final int scale = Math.max(decimal.scale(), defaultFractionDigits);
+
+ return decimal.setScale(scale, RoundingMode.UNNECESSARY);
+ }
+
+}
diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/FieldNames.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/FieldNames.java
new file mode 100644
index 0000000..18d222e
--- /dev/null
+++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/FieldNames.java
@@ -0,0 +1,26 @@
+package com.fasterxml.jackson.datatype.javax.money;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.With;
+
+@AllArgsConstructor(staticName = "valueOf")
+@Getter
+public final class FieldNames {
+
+ static final FieldNames DEFAULT = FieldNames.valueOf("amount", "currency", "formatted");
+
+ @With
+ private final String amount;
+
+ @With
+ private final String currency;
+
+ @With
+ private final String formatted;
+
+ public static FieldNames defaults() {
+ return DEFAULT;
+ }
+
+}
diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/JavaxMoneyModule.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/JavaxMoneyModule.java
new file mode 100644
index 0000000..1164baa
--- /dev/null
+++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/JavaxMoneyModule.java
@@ -0,0 +1,110 @@
+package com.fasterxml.jackson.datatype.javax.money;
+
+import javax.money.CurrencyUnit;
+import javax.money.Monetary;
+import javax.money.MonetaryAmount;
+import javax.money.format.MonetaryFormats;
+
+import org.apiguardian.api.API;
+
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.module.SimpleDeserializers;
+import com.fasterxml.jackson.databind.module.SimpleSerializers;
+
+import static org.apiguardian.api.API.Status.EXPERIMENTAL;
+import static org.apiguardian.api.API.Status.STABLE;
+
+@API(status = STABLE)
+public final class JavaxMoneyModule extends Module {
+
+ private final AmountWriter> writer;
+ private final FieldNames names;
+ private final MonetaryAmountFormatFactory formatFactory;
+ private final MonetaryAmountFactory extends MonetaryAmount> amountFactory;
+
+ public JavaxMoneyModule() {
+ //When No AmountFactory is provided, use the default MonetaryAmountFactory from Monetary
+ this(new DecimalAmountWriter(), FieldNames.defaults(), MonetaryAmountFormatFactory.NONE, (amount, currency) -> Monetary.getDefaultAmountFactory().setNumber(amount).setCurrency(currency).create());
+ }
+
+ private JavaxMoneyModule(final AmountWriter> writer, final FieldNames names, final MonetaryAmountFormatFactory formatFactory, final MonetaryAmountFactory amountFactory) {
+
+ this.writer = writer;
+ this.names = names;
+ this.formatFactory = formatFactory;
+ this.amountFactory = amountFactory;
+ }
+
+ @Override
+ public String getModuleName() {
+ return JavaxMoneyModule.class.getSimpleName();
+ }
+
+ @Override
+ public Version version() {
+ return PackageVersion.VERSION;
+ }
+
+ @Override
+ public void setupModule(final SetupContext context) {
+ final SimpleSerializers serializers = new SimpleSerializers();
+ serializers.addSerializer(CurrencyUnit.class, new CurrencyUnitSerializer());
+ serializers.addSerializer(MonetaryAmount.class, new MonetaryAmountSerializer(names, writer, formatFactory));
+ context.addSerializers(serializers);
+
+ final SimpleDeserializers deserializers = new SimpleDeserializers();
+ deserializers.addDeserializer(CurrencyUnit.class, new CurrencyUnitDeserializer());
+
+ //Use provided amountFactory to deserialize a MonetaryAmount
+ deserializers.addDeserializer(MonetaryAmount.class, new MonetaryAmountDeserializer<>(amountFactory, names));
+
+ context.addDeserializers(deserializers);
+ }
+
+ public JavaxMoneyModule withDecimalNumbers() {
+ return withNumbers(new DecimalAmountWriter());
+ }
+
+ public JavaxMoneyModule withQuotedDecimalNumbers() {
+ return withNumbers(new QuotedDecimalAmountWriter());
+ }
+
+ @API(status = EXPERIMENTAL)
+ public JavaxMoneyModule withNumbers(final AmountWriter> writer) {
+ return new JavaxMoneyModule(writer, names, formatFactory, amountFactory);
+ }
+
+ public JavaxMoneyModule withMonetaryAmountFactory(final MonetaryAmountFactory amountFactory) {
+ return new JavaxMoneyModule(writer, names, formatFactory, amountFactory);
+ }
+
+ public JavaxMoneyModule withoutFormatting() {
+ return withFormatting(MonetaryAmountFormatFactory.NONE);
+ }
+
+ public JavaxMoneyModule withDefaultFormatting() {
+ return withFormatting(MonetaryFormats::getAmountFormat);
+ }
+
+ public JavaxMoneyModule withFormatting(final MonetaryAmountFormatFactory formatFactory) {
+ return new JavaxMoneyModule(writer, names, formatFactory, amountFactory);
+ }
+
+ public JavaxMoneyModule withAmountFieldName(final String name) {
+ return withFieldNames(names.withAmount(name));
+ }
+
+ public JavaxMoneyModule withCurrencyFieldName(final String name) {
+ return withFieldNames(names.withCurrency(name));
+ }
+
+ public JavaxMoneyModule withFormattedFieldName(final String name) {
+ return withFieldNames(names.withFormatted(name));
+ }
+
+ private JavaxMoneyModule withFieldNames(final FieldNames names) {
+ return new JavaxMoneyModule(writer, names, formatFactory, amountFactory);
+ }
+
+}
\ No newline at end of file
diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/MonetaryAmountDeserializer.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/MonetaryAmountDeserializer.java
new file mode 100644
index 0000000..d4691fe
--- /dev/null
+++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/MonetaryAmountDeserializer.java
@@ -0,0 +1,77 @@
+package com.fasterxml.jackson.datatype.javax.money;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
+import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
+
+import javax.money.CurrencyUnit;
+import javax.money.MonetaryAmount;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.Objects;
+
+import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
+import static java.lang.String.format;
+
+public final class MonetaryAmountDeserializer extends JsonDeserializer {
+
+ private final MonetaryAmountFactory factory;
+ private final FieldNames names;
+
+ public MonetaryAmountDeserializer(final MonetaryAmountFactory factory, final FieldNames names) {
+ this.factory = factory;
+ this.names = names;
+ }
+
+ @Override
+ public Object deserializeWithType(final JsonParser parser, final DeserializationContext context,
+ final TypeDeserializer deserializer) throws IOException {
+
+ // effectively assuming no type information at all
+ return deserialize(parser, context);
+ }
+
+ @Override
+ public M deserialize(final JsonParser parser, final DeserializationContext context) throws IOException {
+ BigDecimal amount = null;
+ CurrencyUnit currency = null;
+
+ while (parser.nextToken() != JsonToken.END_OBJECT) {
+ final String field = parser.getCurrentName();
+
+ parser.nextToken();
+
+ if (field.equals(names.getAmount())) {
+ amount = context.readValue(parser, BigDecimal.class);
+ } else if (field.equals(names.getCurrency())) {
+ currency = context.readValue(parser, CurrencyUnit.class);
+ } else if (field.equals(names.getFormatted())) {
+ //noinspection UnnecessaryContinue
+ continue;
+ } else if (context.isEnabled(FAIL_ON_UNKNOWN_PROPERTIES)) {
+ throw UnrecognizedPropertyException.from(parser, MonetaryAmount.class, field,
+ Arrays.asList(names.getAmount(), names.getCurrency(), names.getFormatted()));
+ } else {
+ parser.skipChildren();
+ }
+ }
+
+ String missingName;
+
+ if (Objects.isNull(currency)) {
+ missingName = names.getCurrency();
+ } else if (Objects.isNull(amount)) {
+ missingName = names.getAmount();
+ } else {
+ return factory.create(amount, currency);
+ }
+
+ return context.reportPropertyInputMismatch(MonetaryAmount.class, missingName, format("Missing property: '%s'", missingName));
+
+ }
+
+}
diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/MonetaryAmountFactory.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/MonetaryAmountFactory.java
new file mode 100644
index 0000000..ebefd61
--- /dev/null
+++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/MonetaryAmountFactory.java
@@ -0,0 +1,17 @@
+package com.fasterxml.jackson.datatype.javax.money;
+
+import org.apiguardian.api.API;
+
+import javax.money.CurrencyUnit;
+import javax.money.MonetaryAmount;
+import java.math.BigDecimal;
+
+import static org.apiguardian.api.API.Status.STABLE;
+
+@API(status = STABLE)
+@FunctionalInterface
+public interface MonetaryAmountFactory {
+
+ M create(BigDecimal amount, CurrencyUnit currency);
+
+}
diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/MonetaryAmountFormatFactory.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/MonetaryAmountFormatFactory.java
new file mode 100644
index 0000000..4ccf091
--- /dev/null
+++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/MonetaryAmountFormatFactory.java
@@ -0,0 +1,20 @@
+package com.fasterxml.jackson.datatype.javax.money;
+
+import org.apiguardian.api.API;
+
+import javax.annotation.Nullable;
+import javax.money.format.MonetaryAmountFormat;
+import java.util.Locale;
+
+import static org.apiguardian.api.API.Status.STABLE;
+
+@API(status = STABLE)
+@FunctionalInterface
+public interface MonetaryAmountFormatFactory {
+
+ MonetaryAmountFormatFactory NONE = locale -> null;
+
+ @Nullable
+ MonetaryAmountFormat create(final Locale defaultLocale);
+
+}
diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/MonetaryAmountSerializer.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/MonetaryAmountSerializer.java
new file mode 100644
index 0000000..102d890
--- /dev/null
+++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/MonetaryAmountSerializer.java
@@ -0,0 +1,124 @@
+package com.fasterxml.jackson.datatype.javax.money;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
+import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor;
+import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+import com.fasterxml.jackson.databind.util.NameTransformer;
+
+import javax.annotation.Nullable;
+import javax.money.CurrencyUnit;
+import javax.money.MonetaryAmount;
+import javax.money.format.MonetaryAmountFormat;
+import java.io.IOException;
+import java.util.Locale;
+
+final class MonetaryAmountSerializer extends StdSerializer
+{
+ private static final long serialVersionUID = 1L;
+
+ private final FieldNames names;
+ private final AmountWriter> writer;
+ private final MonetaryAmountFormatFactory factory;
+ private final boolean isUnwrapping;
+ private final NameTransformer nameTransformer;
+
+ MonetaryAmountSerializer(final FieldNames names, final AmountWriter> writer,
+ final MonetaryAmountFormatFactory factory, boolean isUnwrapping, @Nullable final NameTransformer nameTransformer) {
+ super(MonetaryAmount.class);
+ this.writer = writer;
+ this.factory = factory;
+ this.names = names;
+ this.isUnwrapping = isUnwrapping;
+ this.nameTransformer = nameTransformer;
+ }
+
+ MonetaryAmountSerializer(final FieldNames names, final AmountWriter> writer,
+ final MonetaryAmountFormatFactory factory) {
+ this(names, writer, factory, false, null);
+ }
+
+ @Override
+ public void acceptJsonFormatVisitor(final JsonFormatVisitorWrapper wrapper, final JavaType hint)
+ throws JsonMappingException {
+
+ @Nullable final JsonObjectFormatVisitor visitor = wrapper.expectObjectFormat(hint);
+
+ if (visitor == null) {
+ return;
+ }
+
+ final SerializerProvider provider = wrapper.getProvider();
+
+ visitor.property(names.getAmount(),
+ provider.findValueSerializer(writer.getType()),
+ provider.constructType(writer.getType()));
+
+ visitor.property(names.getCurrency(),
+ provider.findValueSerializer(CurrencyUnit.class),
+ provider.constructType(CurrencyUnit.class));
+
+ visitor.optionalProperty(names.getFormatted(),
+ provider.findValueSerializer(String.class),
+ provider.constructType(String.class));
+ }
+
+ @Override
+ public void serializeWithType(final MonetaryAmount value, final JsonGenerator generator,
+ final SerializerProvider provider, final TypeSerializer serializer) throws IOException {
+
+ // effectively assuming no type information at all
+ serialize(value, generator, provider);
+ }
+
+ @Override
+ public void serialize(final MonetaryAmount value, final JsonGenerator json, final SerializerProvider provider)
+ throws IOException {
+
+ final CurrencyUnit currency = value.getCurrency();
+ @Nullable final String formatted = format(value, provider);
+
+ if (!isUnwrapping) {
+ json.writeStartObject();
+ }
+
+ {
+ provider.defaultSerializeField(transformName(names.getAmount()), writer.write(value), json);
+ provider.defaultSerializeField(transformName(names.getCurrency()), currency, json);
+
+ if (formatted != null) {
+ provider.defaultSerializeField(transformName(names.getFormatted()), formatted, json);
+ }
+ }
+
+ if (!isUnwrapping) {
+ json.writeEndObject();
+ }
+ }
+
+ private String transformName(String name) {
+ return (nameTransformer != null) ? nameTransformer.transform(name) : name;
+ }
+
+ @Nullable
+ private String format(final MonetaryAmount value, final SerializerProvider provider) {
+ final Locale locale = provider.getConfig().getLocale();
+ final MonetaryAmountFormat format = factory.create(locale);
+ return format == null ? null : format.format(value);
+ }
+
+ @Override
+ public boolean isUnwrappingSerializer() {
+ return isUnwrapping;
+ }
+
+ @Override
+ public JsonSerializer unwrappingSerializer(@Nullable final NameTransformer nameTransformer) {
+ return new MonetaryAmountSerializer(names, writer, factory, true, nameTransformer);
+ }
+}
diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/PackageVersion.java.in b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/PackageVersion.java.in
new file mode 100644
index 0000000..7860aa1
--- /dev/null
+++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/PackageVersion.java.in
@@ -0,0 +1,20 @@
+package @package@;
+
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.core.Versioned;
+import com.fasterxml.jackson.core.util.VersionUtil;
+
+/**
+ * Automatically generated from PackageVersion.java.in during
+ * packageVersion-generate execution of maven-replacer-plugin in
+ * pom.xml.
+ */
+public final class PackageVersion implements Versioned {
+ public final static Version VERSION = VersionUtil.parseVersion(
+ "@projectversion@", "@projectgroupid@", "@projectartifactid@");
+
+ @Override
+ public Version version() {
+ return VERSION;
+ }
+}
diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/QuotedDecimalAmountWriter.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/QuotedDecimalAmountWriter.java
new file mode 100644
index 0000000..40aff2e
--- /dev/null
+++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/javax/money/QuotedDecimalAmountWriter.java
@@ -0,0 +1,20 @@
+package com.fasterxml.jackson.datatype.javax.money;
+
+import javax.money.MonetaryAmount;
+import java.math.BigDecimal;
+
+final class QuotedDecimalAmountWriter implements AmountWriter {
+
+ private final AmountWriter delegate = new DecimalAmountWriter();
+
+ @Override
+ public Class getType() {
+ return String.class;
+ }
+
+ @Override
+ public String write(final MonetaryAmount amount) {
+ return delegate.write(amount).toPlainString();
+ }
+
+}
diff --git a/javax-money/src/main/resources/META-INF/LICENSE b/javax-money/src/main/resources/META-INF/LICENSE
new file mode 100644
index 0000000..5bc3370
--- /dev/null
+++ b/javax-money/src/main/resources/META-INF/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2025-2026 Zalando SE
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/javax-money/src/main/resources/META-INF/NOTICE b/javax-money/src/main/resources/META-INF/NOTICE
new file mode 100644
index 0000000..d55c59a
--- /dev/null
+++ b/javax-money/src/main/resources/META-INF/NOTICE
@@ -0,0 +1,17 @@
+# Jackson JSON processor
+
+Jackson is a high-performance, Free/Open Source JSON processing library.
+It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has
+been in development since 2007.
+It is currently developed by a community of developers.
+
+## Licensing
+
+Jackson components are licensed under Apache (Software) License, version 2.0,
+as per accompanying LICENSE file.
+
+## Credits
+
+A list of contributors may be found from CREDITS file, which is included
+in some artifacts (usually source distributions); but is always available
+from the source code management (SCM) system project uses.
diff --git a/javax-money/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module b/javax-money/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module
new file mode 100644
index 0000000..bf0fc8b
--- /dev/null
+++ b/javax-money/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module
@@ -0,0 +1 @@
+com.fasterxml.jackson.datatype.javax.money.JavaxMoneyModule
diff --git a/javax-money/src/moditect/module-info.java b/javax-money/src/moditect/module-info.java
new file mode 100644
index 0000000..71ce520
--- /dev/null
+++ b/javax-money/src/moditect/module-info.java
@@ -0,0 +1,13 @@
+// Hand-crafted 05-Nov-2024
+module com.fasterxml.jackson.datatype.javax.money
+{
+ requires com.fasterxml.jackson.annotation;
+ requires com.fasterxml.jackson.core;
+ requires com.fasterxml.jackson.databind;
+ requires javax.money;
+
+ exports com.fasterxml.jackson.datatype.javax.money;
+
+ provides com.fasterxml.jackson.databind.Module with
+ com.fasterxml.jackson.datatype.javax.money.JavaxMoneyModule;
+}
diff --git a/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/CurrencyUnitDeserializerTest.java b/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/CurrencyUnitDeserializerTest.java
new file mode 100644
index 0000000..c944ebd
--- /dev/null
+++ b/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/CurrencyUnitDeserializerTest.java
@@ -0,0 +1,46 @@
+package com.fasterxml.jackson.datatype.javax.money;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
+import com.fasterxml.jackson.datatype.javax.money.JavaxMoneyModule;
+
+import org.javamoney.moneta.CurrencyUnitBuilder;
+import org.junit.jupiter.api.Test;
+
+import javax.money.CurrencyUnit;
+import javax.money.UnknownCurrencyException;
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public final class CurrencyUnitDeserializerTest {
+
+ private final ObjectMapper unit = JsonMapper.builder().addModule(new JavaxMoneyModule()).build();
+
+ @Test
+ public void shouldDeserialize() throws IOException {
+ final CurrencyUnit actual = unit.readValue("\"EUR\"", CurrencyUnit.class);
+ final CurrencyUnit expected = CurrencyUnitBuilder.of("EUR", "default").build();
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ public void shouldNotDeserializeInvalidCurrency() {
+ assertThrows(UnknownCurrencyException.class, () ->
+ unit.readValue("\"FOO\"", CurrencyUnit.class));
+ }
+
+ @Test
+ public void shouldDeserializeWithTyping() throws IOException {
+ unit.activateDefaultTyping(BasicPolymorphicTypeValidator.builder().build());
+
+ final CurrencyUnit actual = unit.readValue("\"EUR\"", CurrencyUnit.class);
+ final CurrencyUnit expected = CurrencyUnitBuilder.of("EUR", "default").build();
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+}
diff --git a/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/CurrencyUnitSchemaSerializerTest.java b/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/CurrencyUnitSchemaSerializerTest.java
new file mode 100644
index 0000000..bf24f7b
--- /dev/null
+++ b/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/CurrencyUnitSchemaSerializerTest.java
@@ -0,0 +1,24 @@
+package com.fasterxml.jackson.datatype.javax.money;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.javax.money.JavaxMoneyModule;
+import com.kjetland.jackson.jsonSchema.JsonSchemaGenerator;
+import org.junit.jupiter.api.Test;
+
+import javax.money.CurrencyUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public final class CurrencyUnitSchemaSerializerTest {
+
+ private final ObjectMapper unit = new ObjectMapper().registerModule(new JavaxMoneyModule());
+
+ @Test
+ public void shouldSerializeJsonSchema() {
+ JsonSchemaGenerator generator = new JsonSchemaGenerator(unit);
+ JsonNode schemaNode = generator.generateJsonSchema(CurrencyUnit.class);
+ assertThat(schemaNode.get("type")).isNotNull();
+ assertThat(schemaNode.get("type").asText()).isEqualTo("string");
+ }
+}
diff --git a/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/CurrencyUnitSerializerTest.java b/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/CurrencyUnitSerializerTest.java
new file mode 100644
index 0000000..e3a345b
--- /dev/null
+++ b/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/CurrencyUnitSerializerTest.java
@@ -0,0 +1,30 @@
+package com.fasterxml.jackson.datatype.javax.money;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.javax.money.JavaxMoneyModule;
+
+import org.javamoney.moneta.CurrencyUnitBuilder;
+import org.junit.jupiter.api.Test;
+
+import javax.money.CurrencyUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+
+public final class CurrencyUnitSerializerTest {
+
+ private final ObjectMapper unit = JsonMapper.builder().addModule(new JavaxMoneyModule()).build();
+
+ @Test
+ public void shouldSerialize() throws JsonProcessingException {
+ final String expected = "EUR";
+ final CurrencyUnit currency = CurrencyUnitBuilder.of(expected, "default").build();
+
+ final String actual = unit.writeValueAsString(currency);
+
+ assertThat(actual).isEqualTo('"' + expected + '"');
+ }
+
+}
diff --git a/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/FieldNamesTest.java b/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/FieldNamesTest.java
new file mode 100644
index 0000000..7ae122c
--- /dev/null
+++ b/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/FieldNamesTest.java
@@ -0,0 +1,23 @@
+package com.fasterxml.jackson.datatype.javax.money;
+
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.datatype.javax.money.FieldNames;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public final class FieldNamesTest {
+
+ @Test
+ public void shouldOptimizeWithMethods() {
+ final FieldNames expected = FieldNames.defaults();
+ final FieldNames actual = expected
+ .withAmount(expected.getAmount())
+ .withCurrency(expected.getCurrency())
+ .withFormatted(expected.getFormatted());
+
+ assertThat(actual).isSameAs(expected);
+ }
+
+}
diff --git a/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/MonetaryAmountDeserializerTest.java b/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/MonetaryAmountDeserializerTest.java
new file mode 100644
index 0000000..b113c5d
--- /dev/null
+++ b/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/MonetaryAmountDeserializerTest.java
@@ -0,0 +1,289 @@
+
+package com.fasterxml.jackson.datatype.javax.money;
+
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.exc.MismatchedInputException;
+import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
+import com.fasterxml.jackson.datatype.javax.money.JavaxMoneyModule;
+
+import org.javamoney.moneta.FastMoney;
+import org.javamoney.moneta.Money;
+import org.javamoney.moneta.RoundedMoney;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import javax.money.Monetary;
+import javax.money.MonetaryAmount;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.List;
+
+import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+public final class MonetaryAmountDeserializerTest {
+
+ @SuppressWarnings("unused")
+ private static List data() {
+ return Arrays.asList(
+ arguments((Configurer) module -> module),
+ arguments((Configurer) module -> new JavaxMoneyModule().withMonetaryAmountFactory(FastMoney::of)),
+ arguments((Configurer) module -> new JavaxMoneyModule().withMonetaryAmountFactory(Money::of)),
+ arguments((Configurer) module -> new JavaxMoneyModule().withMonetaryAmountFactory(RoundedMoney::of)),
+ arguments((Configurer) module ->
+ new JavaxMoneyModule().withMonetaryAmountFactory((amount, currency) ->
+ RoundedMoney.of(amount, currency, Monetary.getDefaultRounding()))
+
+ )
+ );
+ }
+
+ private interface Configurer {
+ JavaxMoneyModule configure(JavaxMoneyModule module);
+ }
+
+ private ObjectMapper unit(final Configurer configurer) {
+ return unit(module(configurer));
+ }
+
+ private ObjectMapper unit(final Module module) {
+ return new ObjectMapper().registerModule(module);
+ }
+
+ private JavaxMoneyModule module(final Configurer configurer) {
+ return configurer.configure(new JavaxMoneyModule());
+ }
+
+
+ @Test
+ public void shouldDeserializeMoneyByDefault() throws IOException {
+ final ObjectMapper unit = JsonMapper.builder().addModule(new JavaxMoneyModule()).build();
+
+ final String content = "{\"amount\":29.95,\"currency\":\"EUR\"}";
+ final MonetaryAmount amount = unit.readValue(content, MonetaryAmount.class);
+
+ assertThat(amount).isInstanceOf(Money.class);
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldDeserialize(final Configurer configurer) throws IOException {
+ final ObjectMapper unit = unit(configurer);
+
+ final String content = "{\"amount\":29.95,\"currency\":\"EUR\"}";
+ final MonetaryAmount amount = unit.readValue(content, MonetaryAmount.class);
+
+ assertThat(amount.getNumber().numberValueExact(BigDecimal.class)).isEqualTo(new BigDecimal("29.95"));
+ assertThat(amount.getCurrency().getCurrencyCode()).isEqualTo("EUR");
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldDeserializeWithHighNumberOfFractionDigits(
+ final Configurer configurer) throws IOException {
+ final ObjectMapper unit = unit(configurer);
+
+ final String content = "{\"amount\":29.9501,\"currency\":\"EUR\"}";
+ final MonetaryAmount amount = unit.readValue(content, MonetaryAmount.class);
+
+ assertThat(amount.getNumber().numberValueExact(BigDecimal.class)).isEqualTo(new BigDecimal("29.9501"));
+ assertThat(amount.getCurrency().getCurrencyCode()).isEqualTo("EUR");
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldDeserializeCorrectlyWhenAmountIsAStringValue(
+ final Configurer configurer) throws IOException {
+ final ObjectMapper unit = unit(configurer);
+
+ final String content = "{\"currency\":\"EUR\",\"amount\":\"29.95\"}";
+ final MonetaryAmount amount = unit.readValue(content, MonetaryAmount.class);
+
+ assertThat(amount.getNumber().numberValueExact(BigDecimal.class)).isEqualTo(new BigDecimal("29.95"));
+ assertThat(amount.getCurrency().getCurrencyCode()).isEqualTo(("EUR"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldDeserializeCorrectlyWhenPropertiesAreInDifferentOrder(
+ final Configurer configurer) throws IOException {
+ final ObjectMapper unit = unit(configurer);
+
+ final String content = "{\"currency\":\"EUR\",\"amount\":29.95}";
+ final MonetaryAmount amount = unit.readValue(content, MonetaryAmount.class);
+
+ assertThat(amount.getNumber().numberValueExact(BigDecimal.class)).isEqualTo((new BigDecimal("29.95")));
+ assertThat(amount.getCurrency().getCurrencyCode()).isEqualTo(("EUR"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldDeserializeWithCustomNames(final Configurer configurer) throws IOException {
+ final ObjectMapper unit = unit(module(configurer)
+ .withAmountFieldName("value")
+ .withCurrencyFieldName("unit"));
+
+ final String content = "{\"value\":29.95,\"unit\":\"EUR\"}";
+ final MonetaryAmount amount = unit.readValue(content, MonetaryAmount.class);
+
+ assertThat(amount.getNumber().numberValueExact(BigDecimal.class)).isEqualTo((new BigDecimal("29.95")));
+ assertThat(amount.getCurrency().getCurrencyCode()).isEqualTo(("EUR"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldIgnoreFormattedValue(final Configurer configurer) throws IOException {
+ final ObjectMapper unit = unit(configurer);
+
+ final String content = "{\"amount\":29.95,\"currency\":\"EUR\",\"formatted\":\"30.00 EUR\"}";
+ final MonetaryAmount amount = unit.readValue(content, MonetaryAmount.class);
+
+ assertThat(amount.getNumber().numberValueExact(BigDecimal.class)).isEqualTo((new BigDecimal("29.95")));
+ assertThat(amount.getCurrency().getCurrencyCode()).isEqualTo(("EUR"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldUpdateExistingValueUsingTreeTraversingParser(
+ final Configurer configurer) throws IOException {
+ final ObjectMapper unit = unit(configurer);
+
+ final String content = "{\"amount\":29.95,\"currency\":\"EUR\"}";
+ final MonetaryAmount amount = unit.readValue(content, MonetaryAmount.class);
+
+ assertThat(amount).isNotNull();
+
+ // we need a json node to get a TreeTraversingParser with codec of type ObjectReader
+ final JsonNode ownerNode =
+ unit.readTree("{\"value\":{\"amount\":29.95,\"currency\":\"EUR\",\"formatted\":\"30.00EUR\"}}");
+
+ final Owner owner = new Owner();
+ owner.setValue(amount);
+
+ // try to update
+ final Owner result = unit.readerForUpdating(owner).readValue(ownerNode);
+ assertThat(result).isNotNull();
+ assertThat(result.getValue()).isEqualTo((amount));
+ }
+
+ static class Owner {
+
+ private MonetaryAmount value;
+
+ MonetaryAmount getValue() {
+ return value;
+ }
+
+ void setValue(final MonetaryAmount value) {
+ this.value = value;
+ }
+
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldFailToDeserializeWithoutAmount(final Configurer configurer) {
+ final ObjectMapper unit = unit(configurer);
+
+ final String content = "{\"currency\":\"EUR\"}";
+
+ final JsonProcessingException exception = assertThrows(
+ JsonProcessingException.class, () -> unit.readValue(content, MonetaryAmount.class));
+
+ assertThat(exception.getMessage()).contains("Missing property: 'amount'");
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldFailToDeserializeWithoutCurrency(final Configurer configurer) {
+ final ObjectMapper unit = unit(configurer);
+
+ final String content = "{\"amount\":29.95}";
+
+ final MismatchedInputException exception = assertThrows(
+ MismatchedInputException.class, () -> unit.readValue(content, MonetaryAmount.class));
+
+ assertThat(exception.getMessage()).contains("Missing property: 'currency'");
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldFailToDeserializeWithAdditionalProperties(
+ final Configurer configurer) {
+ final ObjectMapper unit = unit(configurer);
+
+ final String content = "{\"amount\":29.95,\"currency\":\"EUR\",\"version\":\"1\"}";
+
+ final JsonProcessingException exception = assertThrows(
+ UnrecognizedPropertyException.class, () -> unit.readValue(content, MonetaryAmount.class));
+
+ assertThat(exception.getMessage()).startsWith(
+ "Unrecognized field \"version\" (class javax.money.MonetaryAmount), " +
+ "not marked as ignorable (3 known properties: \"amount\", \"currency\", \"formatted\"])");
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldNotFailToDeserializeWithAdditionalProperties(
+ final Configurer configurer) throws IOException {
+ final ObjectMapper unit = unit(configurer).disable(FAIL_ON_UNKNOWN_PROPERTIES);
+
+ final String content = "{\"source\":{\"provider\":\"ECB\",\"date\":\"2016-09-29\"},\"amount\":29.95,\"currency\":\"EUR\",\"version\":\"1\"}";
+ unit.readValue(content, MonetaryAmount.class);
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldDeserializeWithTypeInformation(final Configurer configurer) throws IOException {
+ final ObjectMapper unit = unit(configurer)
+ .activateDefaultTyping(
+ BasicPolymorphicTypeValidator.builder().build(),
+ ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE,
+ JsonTypeInfo.As.EXISTING_PROPERTY)
+ .disable(FAIL_ON_UNKNOWN_PROPERTIES);
+
+ final String content = "{\"type\":\"org.javamoney.moneta.Money\",\"amount\":29.95,\"currency\":\"EUR\"}";
+ final MonetaryAmount amount = unit.readValue(content, MonetaryAmount.class);
+
+ // type information is ignored?!
+ assertThat(amount).isInstanceOf(MonetaryAmount.class);
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldDeserializeWithoutTypeInformation(final Configurer configurer) throws IOException {
+ final ObjectMapper unit = unit(configurer).activateDefaultTyping(
+ BasicPolymorphicTypeValidator.builder().build());
+
+ final String content = "{\"amount\":29.95,\"currency\":\"EUR\"}";
+ final MonetaryAmount amount = unit.readValue(content, MonetaryAmount.class);
+
+ assertThat(amount).isInstanceOf(MonetaryAmount.class);
+ }
+
+ @Test
+ public void shouldDeserializeToAMonetaImplementationWithProvidedFactory() throws JsonProcessingException {
+
+ //Custom FastMoney factory that returns zero
+ final ObjectMapper unit = new ObjectMapper().registerModule(new JavaxMoneyModule().withMonetaryAmountFactory((number, currency) -> (FastMoney.zero(currency))));
+
+ final String content = "{\"amount\":29.95,\"currency\":\"EUR\"}";
+ final MonetaryAmount amount = unit.readValue(content, MonetaryAmount.class);
+
+ assertThat(amount.getNumber().numberValueExact(BigDecimal.class)).isEqualTo(BigDecimal.ZERO);
+ assertThat(amount.getCurrency().getCurrencyCode()).isEqualTo("EUR");
+
+ }
+
+}
diff --git a/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/MonetaryAmountSchemaSerializerTest.java b/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/MonetaryAmountSchemaSerializerTest.java
new file mode 100644
index 0000000..6653cf4
--- /dev/null
+++ b/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/MonetaryAmountSchemaSerializerTest.java
@@ -0,0 +1,85 @@
+package com.fasterxml.jackson.datatype.javax.money;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.javax.money.JavaxMoneyModule;
+import com.kjetland.jackson.jsonSchema.JsonSchemaGenerator;
+import org.junit.jupiter.api.Test;
+
+import javax.money.MonetaryAmount;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public final class MonetaryAmountSchemaSerializerTest {
+
+ @Test
+ public void shouldSerializeJsonSchema() throws Exception {
+ final ObjectMapper unit = unit(module());
+ final JsonSchemaGenerator generator = new JsonSchemaGenerator(unit);
+ final JsonNode jsonSchema = generator.generateJsonSchema(MonetaryAmount.class);
+ final String actual = unit.writeValueAsString(jsonSchema);
+ final String expected = "{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"title\":\"Monetary Amount\"" +
+ ",\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"amount\":{\"type\":\"number\"}" +
+ ",\"currency\":{\"type\":\"string\"},\"formatted\":{\"type\":\"string\"}}" +
+ ",\"required\":[\"amount\",\"currency\"]}";
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ public void shouldSerializeJsonSchemaWithCustomFieldNames() throws Exception {
+ final ObjectMapper unit = unit(module().withAmountFieldName("value")
+ .withCurrencyFieldName("unit")
+ .withFormattedFieldName("pretty"));
+ final JsonSchemaGenerator generator = new JsonSchemaGenerator(unit);
+ final JsonNode jsonSchema = generator.generateJsonSchema(MonetaryAmount.class);
+ final String actual = unit.writeValueAsString(jsonSchema);
+ final String expected = "{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"title\":\"Monetary Amount\"" +
+ ",\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"value\":{\"type\":\"number\"}" +
+ ",\"unit\":{\"type\":\"string\"},\"pretty\":{\"type\":\"string\"}},\"required\":[\"value\",\"unit\"]}";
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ public void shouldSerializeJsonSchemaWithQuotedDecimalNumbers() throws Exception {
+ final ObjectMapper unit = unit(module().withQuotedDecimalNumbers());
+ final JsonSchemaGenerator generator = new JsonSchemaGenerator(unit);
+ final JsonNode jsonSchema = generator.generateJsonSchema(MonetaryAmount.class);
+ final String actual = unit.writeValueAsString(jsonSchema);
+ final String expected = "{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"title\":\"Monetary Amount\"" +
+ ",\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"amount\":{\"type\":\"string\"}" +
+ ",\"currency\":{\"type\":\"string\"},\"formatted\":{\"type\":\"string\"}},\"required\":[\"amount\",\"currency\"]}";
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ public void shouldSerializeJsonSchemaWithMultipleMonetayAmounts() throws Exception {
+ final ObjectMapper unit = unit(module());
+ final com.kjetland.jackson.jsonSchema.JsonSchemaGenerator generator =
+ new com.kjetland.jackson.jsonSchema.JsonSchemaGenerator(unit);
+
+ final JsonNode jsonSchema = generator.generateJsonSchema(SchemaTestClass.class);
+
+ final String actual = unit.writeValueAsString(jsonSchema);
+ final String expected = "{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"title\":\"Schema Test Class\"," +
+ "\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"moneyOne\":{\"$ref\":" +
+ "\"#/definitions/MonetaryAmount\"},\"moneyTwo\":{\"$ref\":\"#/definitions/MonetaryAmount\"}}," +
+ "\"definitions\":{\"MonetaryAmount\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\"" +
+ ":{\"amount\":{\"type\":\"number\"},\"currency\":{\"type\":\"string\"},\"formatted\":" +
+ "{\"type\":\"string\"}},\"required\":[\"amount\",\"currency\"]}}}";
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ private ObjectMapper unit(final Module module) {
+ return new ObjectMapper().registerModule(module);
+ }
+
+ private JavaxMoneyModule module() {
+ return new JavaxMoneyModule();
+ }
+
+}
diff --git a/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/MonetaryAmountSerializerTest.java b/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/MonetaryAmountSerializerTest.java
new file mode 100644
index 0000000..f62ee4a
--- /dev/null
+++ b/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/MonetaryAmountSerializerTest.java
@@ -0,0 +1,382 @@
+package com.fasterxml.jackson.datatype.javax.money;
+
+import com.fasterxml.jackson.annotation.JsonUnwrapped;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.json.JsonWriteFeature;
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
+import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
+import com.fasterxml.jackson.databind.type.SimpleType;
+import com.fasterxml.jackson.datatype.javax.money.AmountWriter;
+import com.fasterxml.jackson.datatype.javax.money.DecimalAmountWriter;
+import com.fasterxml.jackson.datatype.javax.money.FieldNames;
+import com.fasterxml.jackson.datatype.javax.money.JavaxMoneyModule;
+import com.fasterxml.jackson.datatype.javax.money.MonetaryAmountFormatFactory;
+import com.fasterxml.jackson.datatype.javax.money.MonetaryAmountSerializer;
+
+import lombok.Value;
+import org.javamoney.moneta.FastMoney;
+import org.javamoney.moneta.Money;
+import org.javamoney.moneta.RoundedMoney;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import javax.money.MonetaryAmount;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.Locale;
+import java.util.stream.Stream;
+
+import static javax.money.Monetary.getDefaultRounding;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public final class MonetaryAmountSerializerTest {
+
+ static Stream amounts() {
+ return Stream.of(
+ FastMoney.of(29.95, "EUR"),
+ Money.of(29.95, "EUR"),
+ RoundedMoney.of(29.95, "EUR", getDefaultRounding()));
+ }
+
+ static Stream hundreds() {
+ return Stream.of(
+ FastMoney.of(100, "EUR"),
+ Money.of(100, "EUR"),
+ RoundedMoney.of(100, "EUR", getDefaultRounding()));
+ }
+
+ static Stream fractions() {
+ return Stream.of(
+ FastMoney.of(0.0001, "EUR"),
+ Money.of(0.0001, "EUR"),
+ RoundedMoney.of(0.0001, "EUR", getDefaultRounding()));
+ }
+
+ private ObjectMapper unit() {
+ return unit(module());
+ }
+
+ private ObjectMapper unit(final Module module) {
+ return build(module).build();
+ }
+
+ private JsonMapper.Builder build() {
+ return build(module());
+ }
+
+ private JsonMapper.Builder build(final Module module) {
+ return JsonMapper.builder()
+ .addModule(module);
+ }
+
+ private JavaxMoneyModule module() {
+ return new JavaxMoneyModule();
+ }
+
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldSerialize(final MonetaryAmount amount) throws JsonProcessingException {
+ final ObjectMapper unit = unit();
+
+ final String expected = "{\"amount\":29.95,\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(amount);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldSerializeWithoutFormattedValueIfFactoryProducesNull(
+ final MonetaryAmount amount) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module().withoutFormatting());
+
+ final String expected = "{\"amount\":29.95,\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(amount);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldSerializeWithFormattedGermanValue(final MonetaryAmount amount) throws JsonProcessingException {
+ final ObjectMapper unit = unit(new JavaxMoneyModule().withDefaultFormatting());
+
+ final String expected = "{\"amount\":29.95,\"currency\":\"EUR\",\"formatted\":\"29,95 EUR\"}";
+
+ final ObjectWriter writer = unit.writer().with(Locale.GERMANY);
+ final String actual = writer.writeValueAsString(amount);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldSerializeWithFormattedAmericanValue(final MonetaryAmount amount) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module().withDefaultFormatting());
+
+ final String expected = "{\"amount\":29.95,\"currency\":\"USD\",\"formatted\":\"USD29.95\"}";
+
+ final ObjectWriter writer = unit.writer().with(Locale.US);
+ final String actual = writer.writeValueAsString(amount.getFactory().setCurrency("USD").create());
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldSerializeWithCustomName(final MonetaryAmount amount) throws IOException {
+ final ObjectMapper unit = unit(module().withDefaultFormatting()
+ .withAmountFieldName("value")
+ .withCurrencyFieldName("unit")
+ .withFormattedFieldName("pretty"));
+
+ final String expected = "{\"value\":29.95,\"unit\":\"EUR\",\"pretty\":\"29,95 EUR\"}";
+
+ final ObjectWriter writer = unit.writer().with(Locale.GERMANY);
+ final String actual = writer.writeValueAsString(amount);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldSerializeAmountAsDecimal(final MonetaryAmount amount) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module().withDecimalNumbers());
+
+ final String expected = "{\"amount\":29.95,\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(amount);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("hundreds")
+ public void shouldSerializeAmountAsDecimalWithDefaultFractionDigits(
+ final MonetaryAmount hundred) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module().withDecimalNumbers());
+
+ final String expected = "{\"amount\":100.00,\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(hundred);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("fractions")
+ public void shouldSerializeAmountAsDecimalWithHigherNumberOfFractionDigits(
+ final MonetaryAmount fraction) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module().withDecimalNumbers());
+
+ final String expected = "{\"amount\":0.0001,\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(fraction);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("hundreds")
+ public void shouldSerializeAmountAsDecimalWithLowerNumberOfFractionDigits(
+ final MonetaryAmount hundred) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module().withNumbers(new AmountWriter() {
+ @Override
+ public Class getType() {
+ return BigDecimal.class;
+ }
+
+ @Override
+ public BigDecimal write(final MonetaryAmount amount) {
+ return amount.getNumber().numberValueExact(BigDecimal.class).stripTrailingZeros();
+ }
+ }));
+
+ final String expected = "{\"amount\":1E+2,\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(hundred);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldSerializeAmountAsQuotedDecimal(final MonetaryAmount amount) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module().withQuotedDecimalNumbers());
+
+ final String expected = "{\"amount\":\"29.95\",\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(amount);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("hundreds")
+ public void shouldSerializeAmountAsQuotedDecimalWithDefaultFractionDigits(
+ final MonetaryAmount hundred) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module().withQuotedDecimalNumbers());
+
+ final String expected = "{\"amount\":\"100.00\",\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(hundred);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("fractions")
+ public void shouldSerializeAmountAsQuotedDecimalWithHigherNumberOfFractionDigits(
+ final MonetaryAmount fraction) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module().withQuotedDecimalNumbers());
+
+ final String expected = "{\"amount\":\"0.0001\",\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(fraction);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("hundreds")
+ public void shouldSerializeAmountAsQuotedDecimalWithLowerNumberOfFractionDigits(
+ final MonetaryAmount hundred) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module().withNumbers(new AmountWriter() {
+ @Override
+ public Class getType() {
+ return String.class;
+ }
+
+ @Override
+ public String write(final MonetaryAmount amount) {
+ return amount.getNumber().numberValueExact(BigDecimal.class).stripTrailingZeros().toPlainString();
+ }
+ }));
+
+ final String expected = "{\"amount\":\"100\",\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(hundred);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("hundreds")
+ public void shouldSerializeAmountAsQuotedDecimalPlainString(final MonetaryAmount hundred) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module().withQuotedDecimalNumbers());
+ unit.enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN);
+
+ final String expected = "{\"amount\":\"100.00\",\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(hundred);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldWriteNumbersAsStrings(final MonetaryAmount amount) throws JsonProcessingException {
+ final ObjectMapper unit = build()
+ .enable(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS)
+ .build();
+
+ final String expected = "{\"amount\":\"29.95\",\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(amount);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("hundreds")
+ public void shouldWriteNumbersAsPlainStrings(final MonetaryAmount hundred) throws JsonProcessingException {
+ final ObjectMapper unit = build()
+ .enable(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS)
+ .enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN)
+ .build();
+
+ final String expected = "{\"amount\":\"100.00\",\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(hundred);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Value
+ private static class Price {
+ MonetaryAmount amount;
+ }
+
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldSerializeWithType(final MonetaryAmount amount) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module()).activateDefaultTyping(BasicPolymorphicTypeValidator.builder().build());
+
+ final String expected = "{\"amount\":{\"amount\":29.95,\"currency\":\"EUR\"}}";
+ final String actual = unit.writeValueAsString(new Price(amount));
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Value
+ private static class PriceUnwrapped {
+ @JsonUnwrapped
+ MonetaryAmount amount;
+ }
+
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldSerializeWithTypeUnwrapped(final MonetaryAmount amount) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module()).activateDefaultTyping(BasicPolymorphicTypeValidator.builder().build());
+
+ final String expected = "{\"amount\":29.95,\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(new PriceUnwrapped(amount));
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Value
+ private static class PriceUnwrappedTransformedNames {
+ @JsonUnwrapped(prefix = "Price-", suffix = "-Field")
+ MonetaryAmount amount;
+ }
+
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldSerializeWithTypeUnwrappedAndNamesTransformed(final MonetaryAmount amount) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module()).activateDefaultTyping(BasicPolymorphicTypeValidator.builder().build());
+
+ final String expected = "{\"Price-amount-Field\":29.95,\"Price-currency-Field\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(new PriceUnwrappedTransformedNames(amount));
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ public void shouldHandleNullValueFromExpectObjectFormatInSchemaVisitor() throws Exception {
+ final MonetaryAmountSerializer unit = new MonetaryAmountSerializer(FieldNames.defaults(),
+ new DecimalAmountWriter(), MonetaryAmountFormatFactory.NONE);
+
+ final JsonFormatVisitorWrapper wrapper = mock(JsonFormatVisitorWrapper.class);
+ unit.acceptJsonFormatVisitor(wrapper, SimpleType.constructUnsafe(MonetaryAmount.class));
+ }
+
+ /**
+ * Fixes a bug that caused the amount field to be written as
+ *
+ * "amount": {"BigDecimal":12.34}
+ *
+ *
+ * @param amount
+ * @throws JsonProcessingException
+ */
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldSerializeWithWrapRootValue(final MonetaryAmount amount) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module())
+ .configure(SerializationFeature.WRAP_ROOT_VALUE, true);
+
+ final String expected = "{\"Price\":{\"amount\":{\"amount\":29.95,\"currency\":\"EUR\"}}}";
+ final String actual = unit.writeValueAsString(new Price(amount));
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+}
diff --git a/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/SchemaTestClass.java b/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/SchemaTestClass.java
new file mode 100644
index 0000000..a30ab0a
--- /dev/null
+++ b/javax-money/src/test/java/com/fasterxml/jackson/datatype/javax/money/SchemaTestClass.java
@@ -0,0 +1,15 @@
+package com.fasterxml.jackson.datatype.javax.money;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import javax.money.MonetaryAmount;
+
+@AllArgsConstructor
+@Getter
+public class SchemaTestClass {
+
+ private final MonetaryAmount moneyOne;
+ private final MonetaryAmount moneyTwo;
+
+}
diff --git a/moneta/README.md b/moneta/README.md
new file mode 100644
index 0000000..8217cba
--- /dev/null
+++ b/moneta/README.md
@@ -0,0 +1,196 @@
+# Jackson Datatype Moneta
+
+*Jackson Datatype Moneta* is a [Jackson](https://github.com/codehaus/jackson) module to support JSON serialization and deserialization of [JavaMoney](https://github.com/JavaMoney/jsr354-api) data types with special support for JavaMoney's Moneta datatypes.
+
+With this library, it is possible to represent monetary amounts in JSON as follows:
+
+```json
+{
+ "amount": 29.95,
+ "currency": "EUR"
+}
+```
+
+## Features
+
+- enables you to express monetary amounts in JSON
+- can be used in a REST APIs
+- customized field names
+- localization of formatted monetary amounts
+- allows you to implement RESTful API endpoints that format monetary amounts based on the Accept-Language header
+- is unique and flexible
+
+## Dependencies
+
+- Java 8 or higher
+- Any build tool using Maven Central, or direct download
+- Jackson
+- JavaMoney
+
+## Installation
+
+Add the following dependency to your project:
+
+```xml
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-moneta
+ ${jackson-datatype-moneta.version}
+
+```
+
+For ultimate flexibility, this module is compatible with the official version as well as the backport of JavaMoney. The
+actual version will be selected by a profile based on the current JDK version.
+
+## Configuration
+
+Register the module with your `ObjectMapper`:
+
+```java
+ObjectMapper mapper = JsonMapper.builder()
+ .addModule(new MonetaMoneyModule())
+ .build();
+```
+
+Alternatively, you can use the SPI capabilities:
+
+```java
+ObjectMapper mapper = new ObjectMapper()
+ .findAndRegisterModules();
+```
+
+### Serialization
+
+For serialization this module currently supports
+[
+`javax.money.MonetaryAmount`](https://github.com/JavaMoney/jsr354-api/blob/master/src/main/java/javax/money/MonetaryAmount.java)
+and will, by default, serialize it as:
+
+```json
+{
+ "amount": 99.95,
+ "currency": "EUR"
+}
+```
+
+To serialize number as a JSON string, you have to configure the quoted decimal number value serializer:
+
+```java
+ObjectMapper mapper = JsonMapper.builder()
+ .addModule(new MonetaMoneyModule().withQuotedDecimalNumbers())
+ .build();
+```
+
+```json
+{
+ "amount": "99.95",
+ "currency": "EUR"
+}
+```
+
+### Formatting
+
+A special feature for serializing monetary amounts is *formatting*, which is **disabled by default**. To enable it, you
+have to either enable default formatting:
+
+```java
+ObjectMapper mapper = JsonMapper.builder()
+ .addModule(new MonetaMoneyModule().withDefaultFormatting())
+ .build();
+```
+
+... or pass in a `MonetaryAmountFormatFactory` implementation to the `MonetaMoneyModule`:
+
+```java
+ObjectMapper mapper = JsonMapper.builder()
+ .addModule(new MonetaMoneyModule()
+ .withFormatting(new CustomMonetaryAmountFormatFactory()))
+ .build();
+```
+
+The default formatting delegates directly to `MonetaryFormats.getAmountFormat(Locale, String...)`.
+
+Formatting only affects the serialization and can be customized based on the *current* locale, as defined by the
+[
+`SerializationConfig`](https://fasterxml.github.io/jackson-databind/javadoc/2.0.0/com/fasterxml/jackson/databind/SerializationConfig.html#with\(java.util.Locale\)).
+This allows to implement RESTful API endpoints
+that format monetary amounts based on the `Accept-Language` header.
+
+The first example serializes a monetary amount using the `de_DE` locale:
+
+```java
+ObjectWriter writer = mapper.writer().with(Locale.GERMANY);
+writer.writeValueAsString(Money.of(29.95, "EUR"));
+```
+
+```json
+{
+ "amount": 29.95,
+ "currency": "EUR",
+ "formatted": "29,95 EUR"
+}
+```
+
+The following example uses `en_US`:
+
+```java
+ObjectWriter writer = mapper.writer().with(Locale.US);
+writer.writeValueAsString(Money.of(29.95, "USD"));
+```
+
+```json
+{
+ "amount": 29.95,
+ "currency": "USD",
+ "formatted": "USD29.95"
+}
+```
+
+More sophisticated formatting rules can be supported by implementing `MonetaryAmountFormatFactory` directly.
+
+### Deserialization
+
+This module will use `org.javamoney.moneta.Money` as an implementation for `javax.money.MonetaryAmount` by default when
+deserializing money values.
+
+In addition, this module comes with support for all `MonetaryAmount` implementations from Moneta, the reference
+implementation of JavaMoney:
+
+| `MonetaryAmount` Implementation | Factory |
+|-------------------------------------|----------------------------------------------|
+| `org.javamoney.moneta.FastMoney` | `new MonetaMoneyModule().withFastMoney()` |
+| `org.javamoney.moneta.Money` | `new MonetaMoneyModule().withMoney()` |
+| `org.javamoney.moneta.RoundedMoney` | `new MonetaMoneyModule().withRoundedMoney()` | |
+
+Module supports deserialization of amount number from JSON number as well as from JSON string without any special
+configuration required.
+
+### Custom Field Names
+
+As you have seen in the previous examples the `MonetaMoneyModule` uses the field names `amount`, `currency` and
+`formatted`
+by default. Those names can be overridden if desired:
+
+```java
+ObjectMapper mapper = JsonMapper.builder()
+ .addModule(new MonetaMoneyModule()
+ .withAmountFieldName("value")
+ .withCurrencyFieldName("unit")
+ .withFormattedFieldName("pretty"))
+ .build();
+```
+
+## Usage
+
+After registering and configuring the module you're now free to directly use `MonetaryAmount` in your data types:
+
+```java
+import javax.money.MonetaryAmount;
+
+public class Product {
+ private String sku;
+ private MonetaryAmount price;
+ ...
+}
+```
\ No newline at end of file
diff --git a/moneta/pom.xml b/moneta/pom.xml
new file mode 100644
index 0000000..fa88979
--- /dev/null
+++ b/moneta/pom.xml
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+ 4.0.0
+
+ com.fasterxml.jackson.datatype
+ jackson-datatypes-misc-parent
+ 2.19.0-SNAPSHOT
+
+ jackson-datatype-moneta
+ Jackson datatype: moneta (javax.money ref. impl)
+ jar
+ 2.19.0-SNAPSHOT
+ Support for datatypes of Money API spec from JSR 354 (https://javamoney.github.io/api.html)
+
+ https://github.com/FasterXML/jackson-datatypes-misc
+
+
+ MIT License
+ https://opensource.org/licenses/MIT
+ repo
+
+
+
+
+ com/fasterxml/jackson/datatype/moneta
+ ${project.groupId}.moneta
+ 2.0.6
+
+
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-javax-money
+ ${project.version}
+
+
+ javax.money
+ money-api
+ 1.1
+
+
+ org.javamoney
+ moneta
+ 1.4.4
+ pom
+
+
+
+ org.apiguardian
+ apiguardian-api
+ 1.1.2
+ provided
+
+
+ com.google.code.findbugs
+ jsr305
+ 3.0.2
+ provided
+
+
+ org.projectlombok
+ lombok
+ 1.18.34
+ provided
+
+
+
+
+ org.slf4j
+ slf4j-nop
+ ${slf4j.version}
+ test
+
+
+ org.mockito
+ mockito-core
+ 4.5.1
+ test
+
+
+ com.kjetland
+ mbknor-jackson-jsonschema_2.12
+ 1.0.39
+ test
+
+
+ javax.validation
+ validation-api
+ 2.0.1.Final
+ test
+
+
+
+
+
+
+ com.google.code.maven-replacer-plugin
+ replacer
+
+
+ org.moditect
+ moditect-maven-plugin
+
+
+
+
+
diff --git a/moneta/src/main/java/com/fasterxml/jackson/datatype/moneta/MonetaMoneyModule.java b/moneta/src/main/java/com/fasterxml/jackson/datatype/moneta/MonetaMoneyModule.java
new file mode 100644
index 0000000..a8f3eb0
--- /dev/null
+++ b/moneta/src/main/java/com/fasterxml/jackson/datatype/moneta/MonetaMoneyModule.java
@@ -0,0 +1,153 @@
+package com.fasterxml.jackson.datatype.moneta;
+
+import javax.money.MonetaryAmount;
+import javax.money.MonetaryOperator;
+import javax.money.MonetaryRounding;
+import javax.money.format.MonetaryFormats;
+
+import org.apiguardian.api.API;
+import org.javamoney.moneta.FastMoney;
+import org.javamoney.moneta.Money;
+import org.javamoney.moneta.RoundedMoney;
+
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.module.SimpleDeserializers;
+import com.fasterxml.jackson.datatype.javax.money.AmountWriter;
+import com.fasterxml.jackson.datatype.javax.money.FieldNames;
+import com.fasterxml.jackson.datatype.javax.money.JavaxMoneyModule;
+import com.fasterxml.jackson.datatype.javax.money.MonetaryAmountDeserializer;
+import com.fasterxml.jackson.datatype.javax.money.MonetaryAmountFactory;
+import com.fasterxml.jackson.datatype.javax.money.MonetaryAmountFormatFactory;
+
+import static org.apiguardian.api.API.Status.EXPERIMENTAL;
+import static org.apiguardian.api.API.Status.STABLE;
+
+/**
+ * Module that adds support for Moneta types (FastMoney, Money, RoundedMoney) on top of the {@linkplain JavaxMoneyModule}.
+ * Contains helper methods like {@link #withFastMoney()}, {@link #withMoney()}, {@link #withRoundedMoney()} and {@link #withRoundedMoney(MonetaryOperator)} that can be used to configure the module.
+ */
+@API(status = STABLE)
+public final class MonetaMoneyModule extends Module
+{
+ private final JavaxMoneyModule baseModule;
+ private final FieldNames names;
+ private final MonetaryAmountFactory fastMoneyFactory;
+ private final MonetaryAmountFactory moneyFactory;
+ private final MonetaryAmountFactory roundedMoneyFactory;
+
+ public MonetaMoneyModule() {
+ this(new JavaxMoneyModule().withMonetaryAmountFactory(Money::of), FieldNames.defaults(), FastMoney::of, Money::of, RoundedMoney::of);
+ }
+
+ private MonetaMoneyModule(final JavaxMoneyModule baseModule, final FieldNames names, final MonetaryAmountFactory fastMoneyFactory, final MonetaryAmountFactory moneyFactory, final MonetaryAmountFactory roundedMoneyFactory) {
+ this.baseModule = baseModule;
+ this.names = names;
+ this.fastMoneyFactory = fastMoneyFactory;
+ this.moneyFactory = moneyFactory;
+ this.roundedMoneyFactory = roundedMoneyFactory;
+ }
+
+ @Override
+ public String getModuleName() {
+ return JavaxMoneyModule.class.getSimpleName();
+ }
+
+ @Override
+ public Version version() {
+ return PackageVersion.VERSION;
+ }
+
+ @Override
+ public void setupModule(final SetupContext context) {
+
+ this.baseModule.setupModule(context);
+ final SimpleDeserializers deserializers = new SimpleDeserializers();
+
+ //Register deserializers for Moneta types
+ deserializers.addDeserializer(FastMoney.class, new MonetaryAmountDeserializer<>(fastMoneyFactory, names));
+ deserializers.addDeserializer(Money.class, new MonetaryAmountDeserializer<>(moneyFactory, names));
+ deserializers.addDeserializer(RoundedMoney.class, new MonetaryAmountDeserializer<>(roundedMoneyFactory, names));
+
+ context.addDeserializers(deserializers);
+ }
+
+ public MonetaMoneyModule withDecimalNumbers() {
+ return new MonetaMoneyModule(baseModule.withDecimalNumbers(), names, fastMoneyFactory, moneyFactory, roundedMoneyFactory);
+ }
+
+ public MonetaMoneyModule withQuotedDecimalNumbers() {
+ return new MonetaMoneyModule(baseModule.withQuotedDecimalNumbers(), names, fastMoneyFactory, moneyFactory, roundedMoneyFactory);
+ }
+
+ @API(status = EXPERIMENTAL)
+ public MonetaMoneyModule withNumbers(final AmountWriter> writer) {
+ return new MonetaMoneyModule(baseModule.withNumbers(writer), names, fastMoneyFactory, moneyFactory, roundedMoneyFactory);
+ }
+
+ /**
+ * @return new {@link MonetaMoneyModule} using {@link FastMoney}
+ * @see FastMoney
+ */
+ public MonetaMoneyModule withFastMoney() {
+ return withMonetaryAmountFactory(fastMoneyFactory);
+ }
+
+ /**
+ * @return new {@link MonetaMoneyModule} using {@link Money}
+ * @see Money
+ */
+ public MonetaMoneyModule withMoney() {
+ return withMonetaryAmountFactory(moneyFactory);
+ }
+
+ /**
+ * @return new {@link MonetaMoneyModule} using {@link RoundedMoney}
+ * @see RoundedMoney
+ */
+ public MonetaMoneyModule withRoundedMoney() {
+ return withMonetaryAmountFactory(roundedMoneyFactory);
+ }
+
+ /**
+ * @param rounding the rounding operator
+ * @return new {@link MonetaMoneyModule} using {@link RoundedMoney} with the given {@link MonetaryRounding}
+ * @see RoundedMoney
+ */
+ public MonetaMoneyModule withRoundedMoney(final MonetaryOperator rounding) {
+ final MonetaryAmountFactory factory = (amount, currency) -> RoundedMoney.of(amount, currency, rounding);
+
+ return withMonetaryAmountFactory(factory);
+ }
+
+
+ private MonetaMoneyModule withMonetaryAmountFactory(final MonetaryAmountFactory amountFactory) {
+ return new MonetaMoneyModule(baseModule.withMonetaryAmountFactory(amountFactory), names, fastMoneyFactory, moneyFactory, roundedMoneyFactory);
+ }
+
+ public MonetaMoneyModule withoutFormatting() {
+ return withFormatting(MonetaryAmountFormatFactory.NONE);
+ }
+
+ public MonetaMoneyModule withDefaultFormatting() {
+ return withFormatting(MonetaryFormats::getAmountFormat);
+ }
+
+ public MonetaMoneyModule withFormatting(final MonetaryAmountFormatFactory formatFactory) {
+ return new MonetaMoneyModule(baseModule.withFormatting(formatFactory), names, fastMoneyFactory, moneyFactory, roundedMoneyFactory);
+ }
+
+ public MonetaMoneyModule withAmountFieldName(final String name) {
+ return new MonetaMoneyModule(baseModule.withAmountFieldName(name), names.withAmount(name), fastMoneyFactory, moneyFactory, roundedMoneyFactory);
+ }
+
+ public MonetaMoneyModule withCurrencyFieldName(final String name) {
+ return new MonetaMoneyModule(baseModule.withCurrencyFieldName(name), names.withCurrency(name), fastMoneyFactory, moneyFactory, roundedMoneyFactory);
+ }
+
+ public MonetaMoneyModule withFormattedFieldName(final String name) {
+ return new MonetaMoneyModule(baseModule.withFormattedFieldName(name), names.withFormatted(name), fastMoneyFactory, moneyFactory, roundedMoneyFactory);
+ }
+
+
+}
diff --git a/moneta/src/main/java/com/fasterxml/jackson/datatype/moneta/PackageVersion.java.in b/moneta/src/main/java/com/fasterxml/jackson/datatype/moneta/PackageVersion.java.in
new file mode 100644
index 0000000..7860aa1
--- /dev/null
+++ b/moneta/src/main/java/com/fasterxml/jackson/datatype/moneta/PackageVersion.java.in
@@ -0,0 +1,20 @@
+package @package@;
+
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.core.Versioned;
+import com.fasterxml.jackson.core.util.VersionUtil;
+
+/**
+ * Automatically generated from PackageVersion.java.in during
+ * packageVersion-generate execution of maven-replacer-plugin in
+ * pom.xml.
+ */
+public final class PackageVersion implements Versioned {
+ public final static Version VERSION = VersionUtil.parseVersion(
+ "@projectversion@", "@projectgroupid@", "@projectartifactid@");
+
+ @Override
+ public Version version() {
+ return VERSION;
+ }
+}
diff --git a/moneta/src/main/resources/META-INF/LICENSE b/moneta/src/main/resources/META-INF/LICENSE
new file mode 100644
index 0000000..5bc3370
--- /dev/null
+++ b/moneta/src/main/resources/META-INF/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2025-2026 Zalando SE
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/moneta/src/main/resources/META-INF/NOTICE b/moneta/src/main/resources/META-INF/NOTICE
new file mode 100644
index 0000000..d55c59a
--- /dev/null
+++ b/moneta/src/main/resources/META-INF/NOTICE
@@ -0,0 +1,17 @@
+# Jackson JSON processor
+
+Jackson is a high-performance, Free/Open Source JSON processing library.
+It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has
+been in development since 2007.
+It is currently developed by a community of developers.
+
+## Licensing
+
+Jackson components are licensed under Apache (Software) License, version 2.0,
+as per accompanying LICENSE file.
+
+## Credits
+
+A list of contributors may be found from CREDITS file, which is included
+in some artifacts (usually source distributions); but is always available
+from the source code management (SCM) system project uses.
diff --git a/moneta/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module b/moneta/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module
new file mode 100644
index 0000000..bdfda87
--- /dev/null
+++ b/moneta/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module
@@ -0,0 +1 @@
+com.fasterxml.jackson.datatype.moneta.MonetaMoneyModule
diff --git a/moneta/src/moditect/module-info.java b/moneta/src/moditect/module-info.java
new file mode 100644
index 0000000..72e4b15
--- /dev/null
+++ b/moneta/src/moditect/module-info.java
@@ -0,0 +1,14 @@
+// Hand-crafted 29-Jan-2025
+module com.fasterxml.jackson.datatype.moneta
+{
+ requires com.fasterxml.jackson.annotation;
+ requires com.fasterxml.jackson.core;
+ requires com.fasterxml.jackson.databind;
+ requires com.fasterxml.jackson.datatype.javax.money;
+ requires javax.money;
+
+ exports com.fasterxml.jackson.datatype.moneta;
+
+ provides com.fasterxml.jackson.databind.Module with
+ com.fasterxml.jackson.datatype.moneta.MonetaMoneyModule;
+}
diff --git a/moneta/src/test/java/com/fasterxml/jackson/datatype/moneta/CurrencyUnitDeserializerTest.java b/moneta/src/test/java/com/fasterxml/jackson/datatype/moneta/CurrencyUnitDeserializerTest.java
new file mode 100644
index 0000000..8fe3bfe
--- /dev/null
+++ b/moneta/src/test/java/com/fasterxml/jackson/datatype/moneta/CurrencyUnitDeserializerTest.java
@@ -0,0 +1,46 @@
+package com.fasterxml.jackson.datatype.moneta;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
+import com.fasterxml.jackson.datatype.moneta.MonetaMoneyModule;
+
+import org.javamoney.moneta.CurrencyUnitBuilder;
+import org.junit.jupiter.api.Test;
+
+import javax.money.CurrencyUnit;
+import javax.money.UnknownCurrencyException;
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public final class CurrencyUnitDeserializerTest {
+
+ private final ObjectMapper unit = JsonMapper.builder().addModule(new MonetaMoneyModule()).build();
+
+ @Test
+ public void shouldDeserialize() throws IOException {
+ final CurrencyUnit actual = unit.readValue("\"EUR\"", CurrencyUnit.class);
+ final CurrencyUnit expected = CurrencyUnitBuilder.of("EUR", "default").build();
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ public void shouldNotDeserializeInvalidCurrency() {
+ assertThrows(UnknownCurrencyException.class, () ->
+ unit.readValue("\"FOO\"", CurrencyUnit.class));
+ }
+
+ @Test
+ public void shouldDeserializeWithTyping() throws IOException {
+ unit.activateDefaultTyping(BasicPolymorphicTypeValidator.builder().build());
+
+ final CurrencyUnit actual = unit.readValue("\"EUR\"", CurrencyUnit.class);
+ final CurrencyUnit expected = CurrencyUnitBuilder.of("EUR", "default").build();
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+}
diff --git a/moneta/src/test/java/com/fasterxml/jackson/datatype/moneta/CurrencyUnitSchemaSerializerTest.java b/moneta/src/test/java/com/fasterxml/jackson/datatype/moneta/CurrencyUnitSchemaSerializerTest.java
new file mode 100644
index 0000000..df0f5f4
--- /dev/null
+++ b/moneta/src/test/java/com/fasterxml/jackson/datatype/moneta/CurrencyUnitSchemaSerializerTest.java
@@ -0,0 +1,25 @@
+package com.fasterxml.jackson.datatype.moneta;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.moneta.MonetaMoneyModule;
+import com.kjetland.jackson.jsonSchema.JsonSchemaGenerator;
+import org.junit.jupiter.api.Test;
+
+import javax.money.CurrencyUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public final class CurrencyUnitSchemaSerializerTest {
+
+ private final ObjectMapper unit = JsonMapper.builder().addModule(new MonetaMoneyModule()).build();
+
+ @Test
+ public void shouldSerializeJsonSchema() {
+ JsonSchemaGenerator generator = new JsonSchemaGenerator(unit);
+ JsonNode schemaNode = generator.generateJsonSchema(CurrencyUnit.class);
+ assertThat(schemaNode.get("type")).isNotNull();
+ assertThat(schemaNode.get("type").asText()).isEqualTo("string");
+ }
+}
diff --git a/moneta/src/test/java/com/fasterxml/jackson/datatype/moneta/CurrencyUnitSerializerTest.java b/moneta/src/test/java/com/fasterxml/jackson/datatype/moneta/CurrencyUnitSerializerTest.java
new file mode 100644
index 0000000..3d246a5
--- /dev/null
+++ b/moneta/src/test/java/com/fasterxml/jackson/datatype/moneta/CurrencyUnitSerializerTest.java
@@ -0,0 +1,30 @@
+package com.fasterxml.jackson.datatype.moneta;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.moneta.MonetaMoneyModule;
+
+import org.javamoney.moneta.CurrencyUnitBuilder;
+import org.junit.jupiter.api.Test;
+
+import javax.money.CurrencyUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+
+public final class CurrencyUnitSerializerTest {
+
+ private final ObjectMapper unit = JsonMapper.builder().addModule(new MonetaMoneyModule()).build();
+
+ @Test
+ public void shouldSerialize() throws JsonProcessingException {
+ final String expected = "EUR";
+ final CurrencyUnit currency = CurrencyUnitBuilder.of(expected, "default").build();
+
+ final String actual = unit.writeValueAsString(currency);
+
+ assertThat(actual).isEqualTo('"' + expected + '"');
+ }
+
+}
diff --git a/moneta/src/test/java/com/fasterxml/jackson/datatype/moneta/MonetaryAmountDeserializerTest.java b/moneta/src/test/java/com/fasterxml/jackson/datatype/moneta/MonetaryAmountDeserializerTest.java
new file mode 100644
index 0000000..0a1985d
--- /dev/null
+++ b/moneta/src/test/java/com/fasterxml/jackson/datatype/moneta/MonetaryAmountDeserializerTest.java
@@ -0,0 +1,279 @@
+package com.fasterxml.jackson.datatype.moneta;
+
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.exc.MismatchedInputException;
+import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
+import com.fasterxml.jackson.datatype.moneta.MonetaMoneyModule;
+
+import org.javamoney.moneta.FastMoney;
+import org.javamoney.moneta.Money;
+import org.javamoney.moneta.RoundedMoney;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import javax.money.Monetary;
+import javax.money.MonetaryAmount;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.List;
+
+import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+public final class MonetaryAmountDeserializerTest {
+
+ @SuppressWarnings("unused")
+ private static List data() {
+ return Arrays.asList(
+ arguments(Money.class, (Configurer) module -> module),
+ arguments(FastMoney.class, (Configurer) module -> new MonetaMoneyModule().withFastMoney()),
+ arguments(Money.class, (Configurer) module -> new MonetaMoneyModule().withMoney()),
+ arguments(RoundedMoney.class, (Configurer) module -> new MonetaMoneyModule().withRoundedMoney()),
+ arguments(RoundedMoney.class, (Configurer) module -> module.withRoundedMoney(Monetary.getDefaultRounding())));
+ }
+
+ private ObjectMapper unit(final Configurer configurer) {
+ return unit(module(configurer));
+ }
+
+ private ObjectMapper unit(final Module module) {
+ return new ObjectMapper().registerModule(module);
+ }
+
+ private MonetaMoneyModule module(final Configurer configurer) {
+ return configurer.configure(new MonetaMoneyModule());
+ }
+
+ @Test
+ public void shouldDeserializeMoneyByDefault() throws IOException {
+ final ObjectMapper unit = JsonMapper.builder().addModule(new MonetaMoneyModule()).build();
+
+ final String content = "{\"amount\":29.95,\"currency\":\"EUR\"}";
+ final MonetaryAmount amount = unit.readValue(content, MonetaryAmount.class);
+
+ assertThat(amount).isInstanceOf(Money.class);
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldDeserializeToCorrectType(final Class type, final Configurer configurer) throws IOException {
+ final ObjectMapper unit = unit(configurer);
+
+ final String content = "{\"amount\":29.95,\"currency\":\"EUR\"}";
+ final MonetaryAmount amount = unit.readValue(content, type);
+
+ assertThat(amount).isInstanceOf(type);
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldDeserialize(final Class type, final Configurer configurer) throws IOException {
+ final ObjectMapper unit = unit(configurer);
+
+ final String content = "{\"amount\":29.95,\"currency\":\"EUR\"}";
+ final MonetaryAmount amount = unit.readValue(content, type);
+
+ assertThat(amount.getNumber().numberValueExact(BigDecimal.class)).isEqualTo(new BigDecimal("29.95"));
+ assertThat(amount.getCurrency().getCurrencyCode()).isEqualTo("EUR");
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldDeserializeWithHighNumberOfFractionDigits(final Class type,
+ final Configurer configurer) throws IOException {
+ final ObjectMapper unit = unit(configurer);
+
+ final String content = "{\"amount\":29.9501,\"currency\":\"EUR\"}";
+ final MonetaryAmount amount = unit.readValue(content, type);
+
+ assertThat(amount.getNumber().numberValueExact(BigDecimal.class)).isEqualTo(new BigDecimal("29.9501"));
+ assertThat(amount.getCurrency().getCurrencyCode()).isEqualTo("EUR");
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldDeserializeCorrectlyWhenAmountIsAStringValue(final Class type,
+ final Configurer configurer) throws IOException {
+ final ObjectMapper unit = unit(configurer);
+
+ final String content = "{\"currency\":\"EUR\",\"amount\":\"29.95\"}";
+ final MonetaryAmount amount = unit.readValue(content, type);
+
+ assertThat(amount.getNumber().numberValueExact(BigDecimal.class)).isEqualTo(new BigDecimal("29.95"));
+ assertThat(amount.getCurrency().getCurrencyCode()).isEqualTo(("EUR"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldDeserializeCorrectlyWhenPropertiesAreInDifferentOrder(final Class type,
+ final Configurer configurer) throws IOException {
+ final ObjectMapper unit = unit(configurer);
+
+ final String content = "{\"currency\":\"EUR\",\"amount\":29.95}";
+ final MonetaryAmount amount = unit.readValue(content, type);
+
+ assertThat(amount.getNumber().numberValueExact(BigDecimal.class)).isEqualTo((new BigDecimal("29.95")));
+ assertThat(amount.getCurrency().getCurrencyCode()).isEqualTo(("EUR"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldDeserializeWithCustomNames(final Class type, final Configurer configurer) throws IOException {
+ final ObjectMapper unit = unit(module(configurer)
+ .withAmountFieldName("value")
+ .withCurrencyFieldName("unit"));
+
+ final String content = "{\"value\":29.95,\"unit\":\"EUR\"}";
+ final MonetaryAmount amount = unit.readValue(content, type);
+
+ assertThat(amount.getNumber().numberValueExact(BigDecimal.class)).isEqualTo((new BigDecimal("29.95")));
+ assertThat(amount.getCurrency().getCurrencyCode()).isEqualTo(("EUR"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldIgnoreFormattedValue(final Class type, final Configurer configurer) throws IOException {
+ final ObjectMapper unit = unit(configurer);
+
+ final String content = "{\"amount\":29.95,\"currency\":\"EUR\",\"formatted\":\"30.00 EUR\"}";
+ final MonetaryAmount amount = unit.readValue(content, type);
+
+ assertThat(amount.getNumber().numberValueExact(BigDecimal.class)).isEqualTo((new BigDecimal("29.95")));
+ assertThat(amount.getCurrency().getCurrencyCode()).isEqualTo(("EUR"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldUpdateExistingValueUsingTreeTraversingParser(final Class type,
+ final Configurer configurer) throws IOException {
+ final ObjectMapper unit = unit(configurer);
+
+ final String content = "{\"amount\":29.95,\"currency\":\"EUR\"}";
+ final MonetaryAmount amount = unit.readValue(content, type);
+
+ assertThat(amount).isNotNull();
+
+ // we need a json node to get a TreeTraversingParser with codec of type ObjectReader
+ final JsonNode ownerNode =
+ unit.readTree("{\"value\":{\"amount\":29.95,\"currency\":\"EUR\",\"formatted\":\"30.00EUR\"}}");
+
+ final Owner owner = new Owner();
+ owner.setValue(amount);
+
+ // try to update
+ final Owner result = unit.readerForUpdating(owner).readValue(ownerNode);
+ assertThat(result).isNotNull();
+ assertThat(result.getValue()).isEqualTo((amount));
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldFailToDeserializeWithoutAmount(final Class type, final Configurer configurer) {
+ final ObjectMapper unit = unit(configurer);
+
+ final String content = "{\"currency\":\"EUR\"}";
+
+ final JsonProcessingException exception = assertThrows(
+ JsonProcessingException.class, () -> unit.readValue(content, type));
+
+ assertThat(exception.getMessage()).contains("Missing property: 'amount'");
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldFailToDeserializeWithoutCurrency(final Class type, final Configurer configurer) {
+ final ObjectMapper unit = unit(configurer);
+
+ final String content = "{\"amount\":29.95}";
+
+ final MismatchedInputException exception = assertThrows(
+ MismatchedInputException.class, () -> unit.readValue(content, type));
+
+ assertThat(exception.getMessage()).contains("Missing property: 'currency'");
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldFailToDeserializeWithAdditionalProperties(final Class type,
+ final Configurer configurer) {
+ final ObjectMapper unit = unit(configurer);
+
+ final String content = "{\"amount\":29.95,\"currency\":\"EUR\",\"version\":\"1\"}";
+
+ final JsonProcessingException exception = assertThrows(
+ UnrecognizedPropertyException.class, () -> unit.readValue(content, type));
+
+ assertThat(exception.getMessage()).startsWith(
+ "Unrecognized field \"version\" (class javax.money.MonetaryAmount), " +
+ "not marked as ignorable (3 known properties: \"amount\", \"currency\", \"formatted\"])");
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldNotFailToDeserializeWithAdditionalProperties(final Class type,
+ final Configurer configurer) throws IOException {
+ final ObjectMapper unit = unit(configurer).disable(FAIL_ON_UNKNOWN_PROPERTIES);
+
+ final String content = "{\"source\":{\"provider\":\"ECB\",\"date\":\"2016-09-29\"},\"amount\":29.95,\"currency\":\"EUR\",\"version\":\"1\"}";
+ unit.readValue(content, type);
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldDeserializeWithTypeInformation(final Class type, final Configurer configurer) throws IOException {
+ final ObjectMapper unit = unit(configurer)
+ .activateDefaultTyping(
+ BasicPolymorphicTypeValidator.builder().build(),
+ ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE,
+ JsonTypeInfo.As.EXISTING_PROPERTY)
+ .disable(FAIL_ON_UNKNOWN_PROPERTIES);
+
+ final String content = "{\"type\":\"org.javamoney.moneta.Money\",\"amount\":29.95,\"currency\":\"EUR\"}";
+ final M amount = unit.readValue(content, type);
+
+ // type information is ignored?!
+ assertThat(amount).isInstanceOf(type);
+ }
+
+ @ParameterizedTest
+ @MethodSource("data")
+ public void shouldDeserializeWithoutTypeInformation(final Class type, final Configurer configurer) throws IOException {
+ final ObjectMapper unit = unit(configurer).activateDefaultTyping(
+ BasicPolymorphicTypeValidator.builder().build());
+
+ final String content = "{\"amount\":29.95,\"currency\":\"EUR\"}";
+ final M amount = unit.readValue(content, type);
+
+ assertThat(amount).isInstanceOf(type);
+ }
+
+ interface Configurer {
+ MonetaMoneyModule configure(MonetaMoneyModule module);
+ }
+
+ static class Owner {
+
+ private MonetaryAmount value;
+
+ MonetaryAmount getValue() {
+ return value;
+ }
+
+ void setValue(final MonetaryAmount value) {
+ this.value = value;
+ }
+
+ }
+
+}
diff --git a/moneta/src/test/java/com/fasterxml/jackson/datatype/moneta/MonetaryAmountSchemaSerializerTest.java b/moneta/src/test/java/com/fasterxml/jackson/datatype/moneta/MonetaryAmountSchemaSerializerTest.java
new file mode 100644
index 0000000..76dffe7
--- /dev/null
+++ b/moneta/src/test/java/com/fasterxml/jackson/datatype/moneta/MonetaryAmountSchemaSerializerTest.java
@@ -0,0 +1,86 @@
+package com.fasterxml.jackson.datatype.moneta;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.moneta.MonetaMoneyModule;
+import com.kjetland.jackson.jsonSchema.JsonSchemaGenerator;
+import org.junit.jupiter.api.Test;
+
+import javax.money.MonetaryAmount;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public final class MonetaryAmountSchemaSerializerTest {
+
+ @Test
+ public void shouldSerializeJsonSchema() throws Exception {
+ final ObjectMapper unit = unit(module());
+ final JsonSchemaGenerator generator = new JsonSchemaGenerator(unit);
+ final JsonNode jsonSchema = generator.generateJsonSchema(MonetaryAmount.class);
+ final String actual = unit.writeValueAsString(jsonSchema);
+ final String expected = "{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"title\":\"Monetary Amount\"" +
+ ",\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"amount\":{\"type\":\"number\"}" +
+ ",\"currency\":{\"type\":\"string\"},\"formatted\":{\"type\":\"string\"}}" +
+ ",\"required\":[\"amount\",\"currency\"]}";
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ public void shouldSerializeJsonSchemaWithCustomFieldNames() throws Exception {
+ final ObjectMapper unit = unit(module().withAmountFieldName("value")
+ .withCurrencyFieldName("unit")
+ .withFormattedFieldName("pretty"));
+ final JsonSchemaGenerator generator = new JsonSchemaGenerator(unit);
+ final JsonNode jsonSchema = generator.generateJsonSchema(MonetaryAmount.class);
+ final String actual = unit.writeValueAsString(jsonSchema);
+ final String expected = "{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"title\":\"Monetary Amount\"" +
+ ",\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"value\":{\"type\":\"number\"}" +
+ ",\"unit\":{\"type\":\"string\"},\"pretty\":{\"type\":\"string\"}},\"required\":[\"value\",\"unit\"]}";
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ public void shouldSerializeJsonSchemaWithQuotedDecimalNumbers() throws Exception {
+ final ObjectMapper unit = unit(module().withQuotedDecimalNumbers());
+ final JsonSchemaGenerator generator = new JsonSchemaGenerator(unit);
+ final JsonNode jsonSchema = generator.generateJsonSchema(MonetaryAmount.class);
+ final String actual = unit.writeValueAsString(jsonSchema);
+ final String expected = "{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"title\":\"Monetary Amount\"" +
+ ",\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"amount\":{\"type\":\"string\"}" +
+ ",\"currency\":{\"type\":\"string\"},\"formatted\":{\"type\":\"string\"}},\"required\":[\"amount\",\"currency\"]}";
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ public void shouldSerializeJsonSchemaWithMultipleMonetayAmounts() throws Exception {
+ final ObjectMapper unit = unit(module());
+ final JsonSchemaGenerator generator =
+ new JsonSchemaGenerator(unit);
+
+ final JsonNode jsonSchema = generator.generateJsonSchema(SchemaTestClass.class);
+
+ final String actual = unit.writeValueAsString(jsonSchema);
+ final String expected = "{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"title\":\"Schema Test Class\"," +
+ "\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"moneyOne\":{\"$ref\":" +
+ "\"#/definitions/MonetaryAmount\"},\"moneyTwo\":{\"$ref\":\"#/definitions/MonetaryAmount\"}}," +
+ "\"definitions\":{\"MonetaryAmount\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\"" +
+ ":{\"amount\":{\"type\":\"number\"},\"currency\":{\"type\":\"string\"},\"formatted\":" +
+ "{\"type\":\"string\"}},\"required\":[\"amount\",\"currency\"]}}}";
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ private ObjectMapper unit(final Module module) {
+ return JsonMapper.builder().addModule(module).build();
+ }
+
+ private MonetaMoneyModule module() {
+ return new MonetaMoneyModule();
+ }
+
+}
diff --git a/moneta/src/test/java/com/fasterxml/jackson/datatype/moneta/MonetaryAmountSerializerTest.java b/moneta/src/test/java/com/fasterxml/jackson/datatype/moneta/MonetaryAmountSerializerTest.java
new file mode 100644
index 0000000..5f2fbbd
--- /dev/null
+++ b/moneta/src/test/java/com/fasterxml/jackson/datatype/moneta/MonetaryAmountSerializerTest.java
@@ -0,0 +1,366 @@
+package com.fasterxml.jackson.datatype.moneta;
+
+import com.fasterxml.jackson.annotation.JsonUnwrapped;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.json.JsonWriteFeature;
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
+import com.fasterxml.jackson.datatype.javax.money.AmountWriter;
+import com.fasterxml.jackson.datatype.moneta.MonetaMoneyModule;
+
+import lombok.Value;
+import org.javamoney.moneta.FastMoney;
+import org.javamoney.moneta.Money;
+import org.javamoney.moneta.RoundedMoney;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import javax.money.MonetaryAmount;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.Locale;
+
+import static javax.money.Monetary.getDefaultRounding;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public final class MonetaryAmountSerializerTest {
+
+ static Iterable amounts() {
+ return Arrays.asList(
+ FastMoney.of(29.95, "EUR"),
+ Money.of(29.95, "EUR"),
+ RoundedMoney.of(29.95, "EUR", getDefaultRounding()));
+ }
+
+ static Iterable hundreds() {
+ return Arrays.asList(
+ FastMoney.of(100, "EUR"),
+ Money.of(100, "EUR"),
+ RoundedMoney.of(100, "EUR", getDefaultRounding()));
+ }
+
+ static Iterable fractions() {
+ return Arrays.asList(
+ FastMoney.of(0.0001, "EUR"),
+ Money.of(0.0001, "EUR"),
+ RoundedMoney.of(0.0001, "EUR", getDefaultRounding()));
+ }
+
+ private ObjectMapper unit() {
+ return unit(module());
+ }
+
+ private ObjectMapper unit(final Module module) {
+ return build(module).build();
+ }
+
+ private JsonMapper.Builder build() {
+ return build(module());
+ }
+
+ private JsonMapper.Builder build(final Module module) {
+ return JsonMapper.builder()
+ .addModule(module);
+ }
+
+ private MonetaMoneyModule module() {
+ return new MonetaMoneyModule();
+ }
+
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldSerialize(final MonetaryAmount amount) throws JsonProcessingException {
+ final ObjectMapper unit = unit();
+
+ final String expected = "{\"amount\":29.95,\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(amount);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldSerializeWithoutFormattedValueIfFactoryProducesNull(
+ final MonetaryAmount amount) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module().withoutFormatting());
+
+ final String expected = "{\"amount\":29.95,\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(amount);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldSerializeWithFormattedGermanValue(final MonetaryAmount amount) throws JsonProcessingException {
+ final ObjectMapper unit = unit(new MonetaMoneyModule().withDefaultFormatting());
+
+ final String expected = "{\"amount\":29.95,\"currency\":\"EUR\",\"formatted\":\"29,95 EUR\"}";
+
+ final ObjectWriter writer = unit.writer().with(Locale.GERMANY);
+ final String actual = writer.writeValueAsString(amount);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldSerializeWithFormattedAmericanValue(final MonetaryAmount amount) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module().withDefaultFormatting());
+
+ final String expected = "{\"amount\":29.95,\"currency\":\"USD\",\"formatted\":\"USD29.95\"}";
+
+ final ObjectWriter writer = unit.writer().with(Locale.US);
+ final String actual = writer.writeValueAsString(amount.getFactory().setCurrency("USD").create());
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldSerializeWithCustomName(final MonetaryAmount amount) throws IOException {
+ final ObjectMapper unit = unit(module().withDefaultFormatting()
+ .withAmountFieldName("value")
+ .withCurrencyFieldName("unit")
+ .withFormattedFieldName("pretty"));
+
+ final String expected = "{\"value\":29.95,\"unit\":\"EUR\",\"pretty\":\"29,95 EUR\"}";
+
+ final ObjectWriter writer = unit.writer().with(Locale.GERMANY);
+ final String actual = writer.writeValueAsString(amount);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldSerializeAmountAsDecimal(final MonetaryAmount amount) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module().withDecimalNumbers());
+
+ final String expected = "{\"amount\":29.95,\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(amount);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("hundreds")
+ public void shouldSerializeAmountAsDecimalWithDefaultFractionDigits(
+ final MonetaryAmount hundred) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module().withDecimalNumbers());
+
+ final String expected = "{\"amount\":100.00,\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(hundred);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("fractions")
+ public void shouldSerializeAmountAsDecimalWithHigherNumberOfFractionDigits(
+ final MonetaryAmount fraction) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module().withDecimalNumbers());
+
+ final String expected = "{\"amount\":0.0001,\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(fraction);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("hundreds")
+ public void shouldSerializeAmountAsDecimalWithLowerNumberOfFractionDigits(
+ final MonetaryAmount hundred) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module().withNumbers(new AmountWriter() {
+ @Override
+ public Class getType() {
+ return BigDecimal.class;
+ }
+
+ @Override
+ public BigDecimal write(final MonetaryAmount amount) {
+ return amount.getNumber().numberValueExact(BigDecimal.class).stripTrailingZeros();
+ }
+ }));
+
+ final String expected = "{\"amount\":1E+2,\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(hundred);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldSerializeAmountAsQuotedDecimal(final MonetaryAmount amount) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module().withQuotedDecimalNumbers());
+
+ final String expected = "{\"amount\":\"29.95\",\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(amount);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("hundreds")
+ public void shouldSerializeAmountAsQuotedDecimalWithDefaultFractionDigits(
+ final MonetaryAmount hundred) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module().withQuotedDecimalNumbers());
+
+ final String expected = "{\"amount\":\"100.00\",\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(hundred);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("fractions")
+ public void shouldSerializeAmountAsQuotedDecimalWithHigherNumberOfFractionDigits(
+ final MonetaryAmount fraction) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module().withQuotedDecimalNumbers());
+
+ final String expected = "{\"amount\":\"0.0001\",\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(fraction);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("hundreds")
+ public void shouldSerializeAmountAsQuotedDecimalWithLowerNumberOfFractionDigits(
+ final MonetaryAmount hundred) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module().withNumbers(new AmountWriter() {
+ @Override
+ public Class getType() {
+ return String.class;
+ }
+
+ @Override
+ public String write(final MonetaryAmount amount) {
+ return amount.getNumber().numberValueExact(BigDecimal.class).stripTrailingZeros().toPlainString();
+ }
+ }));
+
+ final String expected = "{\"amount\":\"100\",\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(hundred);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("hundreds")
+ public void shouldSerializeAmountAsQuotedDecimalPlainString(final MonetaryAmount hundred) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module().withQuotedDecimalNumbers());
+ unit.enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN);
+
+ final String expected = "{\"amount\":\"100.00\",\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(hundred);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldWriteNumbersAsStrings(final MonetaryAmount amount) throws JsonProcessingException {
+ final ObjectMapper unit = build()
+ .enable(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS)
+ .build();
+
+ final String expected = "{\"amount\":\"29.95\",\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(amount);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("hundreds")
+ public void shouldWriteNumbersAsPlainStrings(final MonetaryAmount hundred) throws JsonProcessingException {
+ final ObjectMapper unit = build()
+ .enable(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS)
+ .enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN)
+ .build();
+
+ final String expected = "{\"amount\":\"100.00\",\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(hundred);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldSerializeWithType(final MonetaryAmount amount) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module()).activateDefaultTyping(BasicPolymorphicTypeValidator.builder().build());
+
+ final String expected = "{\"amount\":{\"amount\":29.95,\"currency\":\"EUR\"}}";
+ final String actual = unit.writeValueAsString(new Price(amount));
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldSerializeWithTypeUnwrapped(final MonetaryAmount amount) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module()).activateDefaultTyping(BasicPolymorphicTypeValidator.builder().build());
+
+ final String expected = "{\"amount\":29.95,\"currency\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(new PriceUnwrapped(amount));
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldSerializeWithTypeUnwrappedAndNamesTransformed(final MonetaryAmount amount) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module()).activateDefaultTyping(BasicPolymorphicTypeValidator.builder().build());
+
+ final String expected = "{\"Price-amount-Field\":29.95,\"Price-currency-Field\":\"EUR\"}";
+ final String actual = unit.writeValueAsString(new PriceUnwrappedTransformedNames(amount));
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ /**
+ * Fixes a bug that caused the amount field to be written as
+ *
+ * "amount": {"BigDecimal":12.34}
+ *
+ *
+ * @param amount
+ * @throws JsonProcessingException
+ */
+ @ParameterizedTest
+ @MethodSource("amounts")
+ public void shouldSerializeWithWrapRootValue(final MonetaryAmount amount) throws JsonProcessingException {
+ final ObjectMapper unit = unit(module())
+ .configure(SerializationFeature.WRAP_ROOT_VALUE, true);
+
+ final String expected = "{\"Price\":{\"amount\":{\"amount\":29.95,\"currency\":\"EUR\"}}}";
+ final String actual = unit.writeValueAsString(new Price(amount));
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Value
+ private static class Price {
+ MonetaryAmount amount;
+ }
+
+ @Value
+ private static class PriceUnwrapped {
+ @JsonUnwrapped
+ MonetaryAmount amount;
+ }
+
+ @Value
+ private static class PriceUnwrappedTransformedNames {
+ @JsonUnwrapped(prefix = "Price-", suffix = "-Field")
+ MonetaryAmount amount;
+ }
+
+}
diff --git a/moneta/src/test/java/com/fasterxml/jackson/datatype/moneta/SchemaTestClass.java b/moneta/src/test/java/com/fasterxml/jackson/datatype/moneta/SchemaTestClass.java
new file mode 100644
index 0000000..06414e8
--- /dev/null
+++ b/moneta/src/test/java/com/fasterxml/jackson/datatype/moneta/SchemaTestClass.java
@@ -0,0 +1,15 @@
+package com.fasterxml.jackson.datatype.moneta;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import javax.money.MonetaryAmount;
+
+@AllArgsConstructor
+@Getter
+public class SchemaTestClass {
+
+ private final MonetaryAmount moneyOne;
+ private final MonetaryAmount moneyTwo;
+
+}
diff --git a/pom.xml b/pom.xml
index a0d7533..2df0eb4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,6 +27,9 @@
jakarta-jsonp
jakarta-mail
+
+ javax-money
+ moneta
https://github.com/FasterXML/jackson-datatypes-misc
diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x
index 2c910ce..45f2c83 100644
--- a/release-notes/VERSION-2.x
+++ b/release-notes/VERSION-2.x
@@ -5,13 +5,15 @@ Modules:
jackson-datatype-jsr353
jackson-datatype-jakarta-jsonp (2.13 alt to -jsr353)
jackson-datatype-jakarta-mail (2.13)
-
+ jackson-datatype-javax-money (2.19)
+ jackson-datatype-moneta (2.19)
------------------------------------------------------------------------
=== Releases ===
------------------------------------------------------------------------
2.19.0 (not yet released)
+#48: Add new "javax.money" (JSR-354) and "moneta" (JSR-354 ref impl) modules
#51: Unify testing structure/tools to JUnit5 [JSTEP-10]
(contributed by Joo-Hyuk K)