From f72d468ef673d3f3583535e58fcd0e9ab1631a18 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Wed, 9 Sep 2026 10:24:57 +0100 Subject: [PATCH 1/3] (javax-money) Report bad `CurrencyUnit` input as databind exception, not `UnknownCurrencyException`/NPE `CurrencyUnitDeserializer` did no token check and no error handling: final String currencyCode = parser.getValueAsString(); return Monetary.getCurrency(currencyCode); so provider exceptions escaped to the caller instead of a `JacksonException`: "XYZQ" -> javax.money.UnknownCurrencyException {"a":1} -> java.lang.NullPointerException: Currency Code may not be null (`getValueAsString()` returns `null` for structured values, which `Monetary` then rejects with an NPE.) Handle both cases the way joda-money's `CurrencyUnitDeserializer` already does: `handleWeirdStringValue()` for an unusable code and `handleUnexpectedToken()` for a non-String token. This also lets a `DeserializationProblemHandler` substitute a value. NOTE: behaviour change. `UnknownCurrencyException` is now reported as `InvalidFormatException`; the two existing tests asserting the old type are updated, in both `javax-money` and `moneta`. Co-Authored-By: Claude Opus 5 (1M context) --- .../javax/money/CurrencyUnitDeserializer.java | 21 +++++++++++-- .../money/CurrencyUnitDeserializerTest.java | 30 +++++++++++++++++-- .../moneta/CurrencyUnitDeserializerTest.java | 30 +++++++++++++++++-- 3 files changed, 73 insertions(+), 8 deletions(-) diff --git a/javax-money/src/main/java/tools/jackson/datatype/javax/money/CurrencyUnitDeserializer.java b/javax-money/src/main/java/tools/jackson/datatype/javax/money/CurrencyUnitDeserializer.java index 6458cb98..9ac4ea22 100644 --- a/javax-money/src/main/java/tools/jackson/datatype/javax/money/CurrencyUnitDeserializer.java +++ b/javax-money/src/main/java/tools/jackson/datatype/javax/money/CurrencyUnitDeserializer.java @@ -4,6 +4,7 @@ import javax.money.Monetary; import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; import tools.jackson.databind.DeserializationContext; import tools.jackson.databind.deser.std.StdScalarDeserializer; import tools.jackson.databind.jsontype.TypeDeserializer; @@ -29,7 +30,23 @@ public Object deserializeWithType(final JsonParser parser, final Deserialization @Override public CurrencyUnit deserialize(final JsonParser parser, final DeserializationContext context) { - final String currencyCode = parser.getValueAsString(); - return Monetary.getCurrency(currencyCode); + if (parser.hasToken(JsonToken.VALUE_STRING)) { + final String currencyCode = parser.getString(); + try { + return Monetary.getCurrency(currencyCode); + } catch (Exception e) { + // 09-Sep-2026, pjfanning: `Monetary` throws `UnknownCurrencyException`, + // which is not a `JacksonException`: convert into standard databind + // exception (and give `DeserializationProblemHandler`s a chance) + return (CurrencyUnit) context.handleWeirdStringValue(handledType(), currencyCode, + e.getMessage()); + } + } + // Anything but String is an error: `getValueAsString()` used to coerce here, and + // returned `null` for structured values -- leading to a bare NPE from `Monetary` + return (CurrencyUnit) context.handleUnexpectedToken(getValueType(context), + parser.currentToken(), parser, + "Expected a `JsonToken.VALUE_STRING`, got `JsonToken.%s`", + parser.currentToken()); } } diff --git a/javax-money/src/test/java/tools/jackson/datatype/javax/money/CurrencyUnitDeserializerTest.java b/javax-money/src/test/java/tools/jackson/datatype/javax/money/CurrencyUnitDeserializerTest.java index fa1edab8..6c93d0fb 100644 --- a/javax-money/src/test/java/tools/jackson/datatype/javax/money/CurrencyUnitDeserializerTest.java +++ b/javax-money/src/test/java/tools/jackson/datatype/javax/money/CurrencyUnitDeserializerTest.java @@ -1,12 +1,13 @@ package tools.jackson.datatype.javax.money; import javax.money.CurrencyUnit; -import javax.money.UnknownCurrencyException; import org.javamoney.moneta.CurrencyUnitBuilder; import org.junit.jupiter.api.Test; import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.exc.InvalidFormatException; +import tools.jackson.databind.exc.MismatchedInputException; import tools.jackson.databind.json.JsonMapper; import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; @@ -27,8 +28,27 @@ public void shouldDeserialize() throws Exception { @Test public void shouldNotDeserializeInvalidCurrency() { - assertThrows(UnknownCurrencyException.class, () -> - unit.readValue("\"FOO\"", CurrencyUnit.class)); + final InvalidFormatException e = assertThrows(InvalidFormatException.class, () -> + unit.readValue(a2q("'FOO'"), CurrencyUnit.class)); + assertThat(e.getMessage()).contains("FOO"); + } + + @Test + public void shouldNotDeserializeFromNumber() { + assertThrows(MismatchedInputException.class, () -> + unit.readValue("12", CurrencyUnit.class)); + } + + @Test + public void shouldNotDeserializeFromObject() { + assertThrows(MismatchedInputException.class, () -> + unit.readValue(a2q("{'a':1}"), CurrencyUnit.class)); + } + + @Test + public void shouldNotDeserializeFromArray() { + assertThrows(MismatchedInputException.class, () -> + unit.readValue("[1,2]", CurrencyUnit.class)); } @Test @@ -43,4 +63,8 @@ public void shouldDeserializeWithTyping() throws Exception { assertThat(actual).isEqualTo(expected); } + + private static String a2q(final String json) { + return json.replace("'", "\""); + } } diff --git a/moneta/src/test/java/tools/jackson/datatype/moneta/CurrencyUnitDeserializerTest.java b/moneta/src/test/java/tools/jackson/datatype/moneta/CurrencyUnitDeserializerTest.java index 9d249fd5..0ba0d664 100644 --- a/moneta/src/test/java/tools/jackson/datatype/moneta/CurrencyUnitDeserializerTest.java +++ b/moneta/src/test/java/tools/jackson/datatype/moneta/CurrencyUnitDeserializerTest.java @@ -1,11 +1,12 @@ package tools.jackson.datatype.moneta; import javax.money.CurrencyUnit; -import javax.money.UnknownCurrencyException; import org.junit.jupiter.api.Test; import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.exc.InvalidFormatException; +import tools.jackson.databind.exc.MismatchedInputException; import tools.jackson.databind.json.JsonMapper; import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; @@ -28,8 +29,27 @@ public void shouldDeserialize() throws Exception { @Test public void shouldNotDeserializeInvalidCurrency() { - assertThrows(UnknownCurrencyException.class, () -> - unit.readValue("\"FOO\"", CurrencyUnit.class)); + final InvalidFormatException e = assertThrows(InvalidFormatException.class, () -> + unit.readValue(a2q("'FOO'"), CurrencyUnit.class)); + assertThat(e.getMessage()).contains("FOO"); + } + + @Test + public void shouldNotDeserializeFromNumber() { + assertThrows(MismatchedInputException.class, () -> + unit.readValue("12", CurrencyUnit.class)); + } + + @Test + public void shouldNotDeserializeFromObject() { + assertThrows(MismatchedInputException.class, () -> + unit.readValue(a2q("{'a':1}"), CurrencyUnit.class)); + } + + @Test + public void shouldNotDeserializeFromArray() { + assertThrows(MismatchedInputException.class, () -> + unit.readValue("[1,2]", CurrencyUnit.class)); } @Test @@ -44,4 +64,8 @@ public void shouldDeserializeWithTyping() throws Exception { assertThat(actual).isEqualTo(expected); } + + private static String a2q(final String json) { + return json.replace("'", "\""); + } } From 3d7801398a2f92bd0caba192ed882bf885967fc9 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Wed, 9 Sep 2026 10:25:11 +0100 Subject: [PATCH 2/3] Add release notes entry for #95 Co-Authored-By: Claude Opus 5 (1M context) --- release-notes/VERSION | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/release-notes/VERSION b/release-notes/VERSION index 6c9ef325..fafb86a2 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -15,7 +15,8 @@ Modules: 3.3.0 (not yet released) -No changes since 3.2 +#95: (javax-money) `CurrencyUnitDeserializer` throws `UnknownCurrencyException` + (and `NullPointerException`) instead of standard databind exception 3.2.2 (14-Aug-2026) From 846eabf76fee83f0a86e4272ff053ea0acdee250 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 14 Sep 2026 20:39:08 -0700 Subject: [PATCH 3/3] ... --- .../datatype/javax/money/CurrencyUnitDeserializerTest.java | 4 ---- release-notes/VERSION | 3 --- 2 files changed, 7 deletions(-) diff --git a/javax-money/src/test/java/tools/jackson/datatype/javax/money/CurrencyUnitDeserializerTest.java b/javax-money/src/test/java/tools/jackson/datatype/javax/money/CurrencyUnitDeserializerTest.java index b7c619e8..736ae639 100644 --- a/javax-money/src/test/java/tools/jackson/datatype/javax/money/CurrencyUnitDeserializerTest.java +++ b/javax-money/src/test/java/tools/jackson/datatype/javax/money/CurrencyUnitDeserializerTest.java @@ -74,8 +74,4 @@ public void shouldDeserializeWithTyping() throws Exception { assertThat(actual).isEqualTo(expected); } - - private static String a2q(final String json) { - return json.replace("'", "\""); - } } diff --git a/release-notes/VERSION b/release-notes/VERSION index 7f83bf4c..266cd575 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -23,9 +23,6 @@ Modules: (contributed by @pjfanning) #93: (moneta) `MonetaMoneyModule.getModuleName()` returns "JavaxMoneyModule" (contributed by @pjfanning) -#95: (javax-money) `CurrencyUnitDeserializer` throws `UnknownCurrencyException` - (and `NullPointerException`) instead of standard databind exception - (contributed by @pjfanning) 3.2.2 (14-Aug-2026)