Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import javax.money.CurrencyUnit;
import javax.money.Monetary;
import javax.money.UnknownCurrencyException;

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;
Expand All @@ -29,7 +31,19 @@ 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);
// [datatypes-misc#91] Only accept String values: for other tokens
// `getValueAsString()` returns `null` (leading to bare NPE) or
// coerces scalars (like numbers) into bogus currency codes
if (!parser.hasToken(JsonToken.VALUE_STRING)) {
return (CurrencyUnit) context.handleUnexpectedToken(getValueType(context), parser);
}
final String currencyCode = parser.getString();
try {
return Monetary.getCurrency(currencyCode);
} catch (UnknownCurrencyException e) {
// [datatypes-misc#91] Report as regular Jackson exception
return (CurrencyUnit) context.handleWeirdStringValue(handledType(), currencyCode,
"not a valid currency code");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
import javax.money.CurrencyUnit;
import javax.money.MonetaryAmount;

import javax.money.CurrencyUnit;
import javax.money.MonetaryAmount;

import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.databind.DeserializationContext;
Expand Down Expand Up @@ -39,10 +36,22 @@ public Object deserializeWithType(final JsonParser parser, final Deserialization
@Override
public M deserialize(final JsonParser parser, final DeserializationContext context)
{
// 09-Sep-2026, pjfanning: [datatypes-misc#91] Verify we got an Object;
// otherwise `currentName()` below returns `null` and we would fail
// with a bare NPE. Besides START_OBJECT, also accept being positioned
// within Object contents (PROPERTY_NAME or END_OBJECT), as may happen
// when caller has already consumed START_OBJECT (and possibly properties)
JsonToken t = parser.currentToken();
if (t == JsonToken.START_OBJECT) {
t = parser.nextToken();
} else if (t != JsonToken.PROPERTY_NAME && t != JsonToken.END_OBJECT) {
return _handleNotObject(parser, context);
}

BigDecimal amount = null;
CurrencyUnit currency = null;

while (parser.nextToken() != JsonToken.END_OBJECT) {
for (; t == JsonToken.PROPERTY_NAME; t = parser.nextToken()) {
final String field = parser.currentName();

parser.nextToken();
Expand All @@ -52,8 +61,8 @@ public M deserialize(final JsonParser parser, final DeserializationContext conte
} else if (field.equals(names.getCurrency())) {
currency = context.readValue(parser, CurrencyUnit.class);
} else if (field.equals(names.getFormatted())) {
//noinspection UnnecessaryContinue
continue;
// [datatypes-misc#91] Skip whole value, which may be structured
parser.skipChildren();
} else if (context.isEnabled(FAIL_ON_UNKNOWN_PROPERTIES)) {
throw UnrecognizedPropertyException.from(parser, MonetaryAmount.class, field,
Arrays.asList(names.getAmount(), names.getCurrency(), names.getFormatted()));
Expand All @@ -75,4 +84,9 @@ public M deserialize(final JsonParser parser, final DeserializationContext conte
return context.reportPropertyInputMismatch(MonetaryAmount.class, missingName,
String.format("Missing property: '%s'", missingName));
}

@SuppressWarnings("unchecked")
private M _handleNotObject(final JsonParser parser, final DeserializationContext context) {
return (M) context.handleUnexpectedToken(MonetaryAmount.class, parser);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package tools.jackson.datatype.javax.money;

import javax.money.CurrencyUnit;
import javax.money.UnknownCurrencyException;
import javax.money.MonetaryAmount;

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;

Expand All @@ -27,8 +29,37 @@ public void shouldDeserialize() throws Exception {

@Test
public void shouldNotDeserializeInvalidCurrency() {
assertThrows(UnknownCurrencyException.class, () ->
final InvalidFormatException e = assertThrows(InvalidFormatException.class, () ->
unit.readValue("\"FOO\"", CurrencyUnit.class));
assertThat(e.getMessage()).contains("javax.money.CurrencyUnit", "\"FOO\"",
"not a valid currency code");
assertThat(e.getValue()).isEqualTo("FOO");
}

@Test
public void shouldNotDeserializeInvalidCurrencyWithinAmount() {
assertThrows(InvalidFormatException.class, () ->
unit.readValue("{\"amount\":1,\"currency\":\"FOO\"}", MonetaryAmount.class));
}

// [datatypes-misc#91] Non-String input must fail with Jackson exception, not NPE
@Test
public void shouldFailOnNonStringInput() {
for (String json : new String[] { "12", "true", "{}", "{\"x\":1}", "[]", "[\"EUR\"]" }) {
final MismatchedInputException e = assertThrows(MismatchedInputException.class,
() -> unit.readValue(json, CurrencyUnit.class), json);
assertThat(e.getMessage()).contains("javax.money.CurrencyUnit");
}
}

@Test
public void shouldFailOnNonStringInputWithinAmount() {
for (String currency : new String[] { "1", "{\"x\":1}", "[\"EUR\"]" }) {
final String json = "{\"amount\":1,\"currency\":" + currency + "}";
final MismatchedInputException e = assertThrows(MismatchedInputException.class,
() -> unit.readValue(json, MonetaryAmount.class), json);
assertThat(e.getMessage()).contains("javax.money.CurrencyUnit");
}
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package tools.jackson.datatype.javax.money;

import java.util.List;

import javax.money.MonetaryAmount;

import tools.jackson.core.JsonParser;
import tools.jackson.core.type.TypeReference;

import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.ValueDeserializer;
import tools.jackson.databind.annotation.JsonDeserialize;
import tools.jackson.databind.exc.MismatchedInputException;
import tools.jackson.databind.json.JsonMapper;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

/**
* Tests to verify that non-Object input is reported as a regular
* {@link MismatchedInputException}, and not as a bare {@link NullPointerException}
* coming out of {@code MonetaryAmountDeserializer}.
*/
public final class FailOnNonObjectTest {

private final ObjectMapper unit = JsonMapper.builder()
.addModule(new JavaxMoneyModule())
.build();

@Test
public void shouldFailOnNumber() {
final MismatchedInputException e = assertThrows(MismatchedInputException.class,
() -> unit.readValue("12", MonetaryAmount.class));
assertThat(e.getMessage()).contains("javax.money.MonetaryAmount", "from Integer value");
}

@Test
public void shouldFailOnString() {
final MismatchedInputException e = assertThrows(MismatchedInputException.class,
() -> unit.readValue(a2q("'abc'"), MonetaryAmount.class));
assertThat(e.getMessage()).contains("javax.money.MonetaryAmount", "from String value");
}

@Test
public void shouldFailOnArray() {
final MismatchedInputException e = assertThrows(MismatchedInputException.class,
() -> unit.readValue("[1,2]", MonetaryAmount.class));
assertThat(e.getMessage()).contains("javax.money.MonetaryAmount", "from Array value");
}

@Test
public void shouldFailOnEmptyArray() {
assertThrows(MismatchedInputException.class,
() -> unit.readValue("[]", MonetaryAmount.class));
}

@Test
public void shouldFailOnBooleanWithinList() {
assertThrows(MismatchedInputException.class,
() -> unit.readValue(a2q("[{'amount':1,'currency':'EUR'},true]"),
new TypeReference<List<MonetaryAmount>>() { }));
}

// ... while valid input keeps working
@Test
public void shouldStillDeserializeObject() {
final MonetaryAmount amount = unit.readValue(a2q("{'amount':29.95,'currency':'EUR'}"),
MonetaryAmount.class);
assertThat(amount.getCurrency().getCurrencyCode()).isEqualTo("EUR");
assertThat(amount.getNumber().doubleValueExact()).isEqualTo(29.95);
}

@Test
public void shouldSkipStructuredFormatted() {
final MonetaryAmount first = unit.readValue(
a2q("{'formatted':[1,2],'amount':1,'currency':'EUR'}"), MonetaryAmount.class);
assertThat(first.getCurrency().getCurrencyCode()).isEqualTo("EUR");

final MonetaryAmount last = unit.readValue(
a2q("{'amount':1,'currency':'EUR','formatted':{'x':[1,{'y':2}]}}"), MonetaryAmount.class);
assertThat(last.getCurrency().getCurrencyCode()).isEqualTo("EUR");

final List<MonetaryAmount> list = unit.readValue(
a2q("[{'amount':1,'currency':'EUR','formatted':[1,2]},{'amount':2,'currency':'USD'}]"),
new TypeReference<List<MonetaryAmount>>() { });
assertThat(list).hasSize(2);
assertThat(list.get(1).getCurrency().getCurrencyCode()).isEqualTo("USD");
}

// Deserializer may also be called when START_OBJECT has already been consumed
@Test
public void shouldDeserializeStartingFromPropertyName() {
final AmountWrapper w = unit.readValue(a2q("{'amount':29.95,'currency':'EUR'}"),
AmountWrapper.class);
assertThat(w.amount.getCurrency().getCurrencyCode()).isEqualTo("EUR");
assertThat(w.amount.getNumber().doubleValueExact()).isEqualTo(29.95);
}

@Test
public void shouldReportMissingPropertiesStartingFromEndObject() {
final MismatchedInputException e = assertThrows(MismatchedInputException.class,
() -> unit.readValue("{}", AmountWrapper.class));
assertThat(e.getMessage()).contains("Missing property");
}

@Test
public void shouldStillDeserializeNull() {
assertThat((MonetaryAmount) unit.readValue("null", MonetaryAmount.class)).isNull();
}

@JsonDeserialize(using = AmountWrapperDeserializer.class)
static final class AmountWrapper {
MonetaryAmount amount;
}

// Skips START_OBJECT and delegates Object contents to MonetaryAmount deserializer
public static final class AmountWrapperDeserializer extends ValueDeserializer<AmountWrapper> {
@Override
public AmountWrapper deserialize(final JsonParser p, final DeserializationContext ctxt) {
p.nextToken();
final AmountWrapper w = new AmountWrapper();
w.amount = (MonetaryAmount) ctxt.findRootValueDeserializer(
ctxt.constructType(MonetaryAmount.class)).deserialize(p, ctxt);
return w;
}
}

private static String a2q(final String json) {
return json.replace("'", "\"");
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
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.json.JsonMapper;
import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator;

Expand All @@ -28,8 +28,9 @@ public void shouldDeserialize() throws Exception {

@Test
public void shouldNotDeserializeInvalidCurrency() {
assertThrows(UnknownCurrencyException.class, () ->
final InvalidFormatException e = assertThrows(InvalidFormatException.class, () ->
unit.readValue("\"FOO\"", CurrencyUnit.class));
assertThat(e.getValue()).isEqualTo("FOO");
}

@Test
Expand Down
3 changes: 3 additions & 0 deletions release-notes/CREDITS
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,8 @@ Christopher Smith (chrylis@github): author of `jakarta-mail` module (added in 2.
* Contributed #90: (json-org) `JSONObject` deserialization quietly returns empty Object
for non-Object input
(3.3.0)
* Contributed #91: (javax-money) `MonetaryAmountDeserializer` throws `NullPointerException`
for non-Object input
(3.3.0)
* Contributed #93: (moneta) `MonetaMoneyModule.getModuleName()` returns "JavaxMoneyModule"
(3.3.0)
3 changes: 3 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ Modules:
#90: (json-org) `JSONObject` deserialization quietly returns empty Object
for non-Object input
(contributed by @pjfanning)
#91: (javax-money) `MonetaryAmountDeserializer` throws `NullPointerException`
for non-Object input
(contributed by @pjfanning)
#93: (moneta) `MonetaMoneyModule.getModuleName()` returns "JavaxMoneyModule"
(contributed by @pjfanning)

Expand Down
Loading